diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 15167cd746c..00000000000 --- a/AUTHORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code refers to The Go Authors for copyright purposes. -# The master list of authors is in the main Go distribution, -# visible at http://tip.golang.org/AUTHORS. diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 1c4577e9680..00000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code was written by the Go contributors. -# The master list of contributors is in the main Go distribution, -# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/README.md b/README.md index 5cd8f0ac6e9..d9d7edd7332 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ Selected commands: - `cmd/toolstash` is a utility to simplify working with multiple versions of the Go toolchain. These commands may be fetched with a command such as -`go install golang.org/x/tools/cmd/goimports@latest`. +``` +go install golang.org/x/tools/cmd/goimports@latest +``` Selected packages: diff --git a/cmd/auth/cookieauth/cookieauth.go b/cmd/auth/cookieauth/cookieauth.go index feefaff0b6e..8b0ff17664b 100644 --- a/cmd/auth/cookieauth/cookieauth.go +++ b/cmd/auth/cookieauth/cookieauth.go @@ -40,7 +40,6 @@ func main() { f, err := os.Open(os.Args[1]) if err != nil { log.Fatalf("failed to read cookie file: %v\n", os.Args[1]) - os.Exit(1) } defer f.Close() diff --git a/cmd/bundle/main.go b/cmd/bundle/main.go index 96cbce9a131..194797bd822 100644 --- a/cmd/bundle/main.go +++ b/cmd/bundle/main.go @@ -84,6 +84,7 @@ import ( "os" "strconv" "strings" + "unicode" "golang.org/x/tools/go/packages" ) @@ -233,7 +234,7 @@ func bundle(src, dst, dstpkg, prefix, buildTags string) ([]byte, error) { fmt.Fprintf(&out, "// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.\n") if *outputFile != "" && buildTags == "" { - fmt.Fprintf(&out, "//go:generate bundle %s\n", strings.Join(os.Args[1:], " ")) + fmt.Fprintf(&out, "//go:generate bundle %s\n", strings.Join(quoteArgs(os.Args[1:]), " ")) } else { fmt.Fprintf(&out, "// $ bundle %s\n", strings.Join(os.Args[1:], " ")) } @@ -447,6 +448,35 @@ func printSameLineComment(out *bytes.Buffer, comments []*ast.CommentGroup, fset return pos } +func quoteArgs(ss []string) []string { + // From go help generate: + // + // > The arguments to the directive are space-separated tokens or + // > double-quoted strings passed to the generator as individual + // > arguments when it is run. + // + // > Quoted strings use Go syntax and are evaluated before execution; a + // > quoted string appears as a single argument to the generator. + // + var qs []string + for _, s := range ss { + if s == "" || containsSpace(s) { + s = strconv.Quote(s) + } + qs = append(qs, s) + } + return qs +} + +func containsSpace(s string) bool { + for _, r := range s { + if unicode.IsSpace(r) { + return true + } + } + return false +} + type flagFunc func(string) func (f flagFunc) Set(s string) error { diff --git a/cmd/compilebench/main.go b/cmd/compilebench/main.go index 28dc450e9e0..abdf28a2d95 100644 --- a/cmd/compilebench/main.go +++ b/cmd/compilebench/main.go @@ -336,9 +336,9 @@ func (compile) long() bool { return false } func (c compile) run(name string, count int) error { // Make sure dependencies needed by go tool compile are installed to GOROOT/pkg. - out, err := exec.Command(*flagGoCmd, "build", "-i", c.dir).CombinedOutput() + out, err := exec.Command(*flagGoCmd, "build", "-a", c.dir).CombinedOutput() if err != nil { - return fmt.Errorf("go build -i %s: %v\n%s", c.dir, err, out) + return fmt.Errorf("go build -a %s: %v\n%s", c.dir, err, out) } // Find dir and source file list. @@ -406,9 +406,9 @@ func (r link) run(name string, count int) error { } // Build dependencies. - out, err := exec.Command(*flagGoCmd, "build", "-i", "-o", "/dev/null", r.dir).CombinedOutput() + out, err := exec.Command(*flagGoCmd, "build", "-a", "-o", "/dev/null", r.dir).CombinedOutput() if err != nil { - return fmt.Errorf("go build -i %s: %v\n%s", r.dir, err, out) + return fmt.Errorf("go build -a %s: %v\n%s", r.dir, err, out) } // Build the main package. diff --git a/cmd/digraph/digraph.go b/cmd/digraph/digraph.go index 62cb08d23a8..69d84ad5012 100644 --- a/cmd/digraph/digraph.go +++ b/cmd/digraph/digraph.go @@ -34,7 +34,7 @@ The support commands are: sccs all strongly connected components (one per line) scc - the set of nodes nodes strongly connected to the specified one + the set of nodes strongly connected to the specified one focus the subgraph containing all directed paths that pass through the specified node diff --git a/cmd/guru/guru.go b/cmd/guru/guru.go index fdb13f92932..18bb0840a1e 100644 --- a/cmd/guru/guru.go +++ b/cmd/guru/guru.go @@ -207,7 +207,7 @@ func pkgContainsFile(bp *build.Package, filename string) byte { return 0 // not found } -// ParseQueryPos parses the source query position pos and returns the +// parseQueryPos parses the source query position pos and returns the // AST node of the loaded program lprog that it identifies. // If needExact, it must identify a single AST subtree; // this is appropriate for queries that allow fairly arbitrary syntax, diff --git a/cmd/splitdwarf/splitdwarf.go b/cmd/splitdwarf/splitdwarf.go index 13888aa512e..9729b0b7a6a 100644 --- a/cmd/splitdwarf/splitdwarf.go +++ b/cmd/splitdwarf/splitdwarf.go @@ -182,7 +182,7 @@ for input_exe need to allow writing. oldsym := symtab.Syms[ii] newsymtab.Syms = append(newsymtab.Syms, oldsym) - linkeditsyms = append(linkeditsyms, macho.Nlist64{Name: uint32(linkeditstringcur), + linkeditsyms = append(linkeditsyms, macho.Nlist64{Name: linkeditstringcur, Type: oldsym.Type, Sect: oldsym.Sect, Desc: oldsym.Desc, Value: oldsym.Value}) linkeditstringcur += uint32(len(oldsym.Name)) + 1 linkeditstrings = append(linkeditstrings, oldsym.Name) diff --git a/cmd/ssadump/main.go b/cmd/ssadump/main.go index 138e7f69ff2..cfb9122b24d 100644 --- a/cmd/ssadump/main.go +++ b/cmd/ssadump/main.go @@ -157,12 +157,15 @@ func doMain() error { // Build SSA for all packages. prog.Build() - // The interpreter needs the runtime package. - // It is a limitation of go/packages that - // we cannot add "runtime" to its initial set, - // we can only check that it is present. - if prog.ImportedPackage("runtime") == nil { - return fmt.Errorf("-run: program does not depend on runtime") + // Earlier versions of the interpreter needed the runtime + // package; however, interp cannot handle unsafe constructs + // used during runtime's package initialization at the moment. + // The key construct blocking support is: + // *((*T)(unsafe.Pointer(p))) + // Unfortunately, this means only trivial programs can be + // interpreted by ssadump. + if prog.ImportedPackage("runtime") != nil { + return fmt.Errorf("-run: program depends on runtime package (interpreter can run only trivial programs)") } if runtime.GOARCH != build.Default.GOARCH { diff --git a/cmd/stringer/stringer.go b/cmd/stringer/stringer.go index 9f9c85a0370..b079985b36c 100644 --- a/cmd/stringer/stringer.go +++ b/cmd/stringer/stringer.go @@ -217,7 +217,7 @@ type Package struct { // parsePackage exits if there is an error. func (g *Generator) parsePackage(patterns []string, tags []string) { cfg := &packages.Config{ - Mode: packages.LoadSyntax, + Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax, // TODO: Need to think about constants in test files. Maybe write type_string_test.go // in a separate pass? For later. Tests: false, diff --git a/cmd/toolstash/buildall b/cmd/toolstash/buildall index 0c6492c9efa..4fc22f7f8fc 100755 --- a/cmd/toolstash/buildall +++ b/cmd/toolstash/buildall @@ -38,10 +38,10 @@ if [ "$pattern" = "" ]; then fi targets="$(go tool dist list; echo linux/386/softfloat)" -targets="$(echo "$targets" | tr '/' '-' | sort | egrep "$pattern" | egrep -v 'android-arm|darwin-arm')" +targets="$(echo "$targets" | tr '/' '-' | sort | grep -E "$pattern" | grep -E -v 'android-arm|darwin-arm')" # put linux first in the target list to get all the architectures up front. -targets="$(echo "$targets" | egrep 'linux') $(echo "$targets" | egrep -v 'linux')" +targets="$(echo "$targets" | grep -E 'linux') $(echo "$targets" | grep -E -v 'linux')" if [ "$sete" = true ]; then set -e diff --git a/container/intsets/sparse.go b/container/intsets/sparse.go index c06aec80b0d..d5fe156ed36 100644 --- a/container/intsets/sparse.go +++ b/container/intsets/sparse.go @@ -190,7 +190,7 @@ func (b *block) min(take bool) int { if take { b.bits[i] = w &^ (1 << uint(tz)) } - return b.offset + int(i*bitsPerWord) + tz + return b.offset + i*bitsPerWord + tz } } panic("BUG: empty block") diff --git a/copyright/copyright.go b/copyright/copyright.go index eb56ef28b22..db63c59922e 100644 --- a/copyright/copyright.go +++ b/copyright/copyright.go @@ -94,7 +94,7 @@ func checkFile(toolsDir, filename string) (bool, error) { return shouldAddCopyright, nil } -// Copied from golang.org/x/tools/internal/lsp/source/util.go. +// Copied from golang.org/x/tools/gopls/internal/lsp/source/util.go. // Matches cgo generated comment as well as the proposed standard: // // https://golang.org/s/generatedcode diff --git a/go.mod b/go.mod index 985b9cc120c..b46da5396a5 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module golang.org/x/tools -go 1.17 +go 1.18 // tagx:compat 1.16 require ( - github.com/yuin/goldmark v1.4.1 - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 - golang.org/x/text v0.3.7 + github.com/yuin/goldmark v1.4.13 + golang.org/x/mod v0.7.0 + golang.org/x/net v0.2.0 + golang.org/x/sys v0.2.0 ) + +require golang.org/x/sync v0.1.0 diff --git a/go.sum b/go.sum index 85cf00cab79..92f0a74888d 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,34 @@ -github.com/yuin/goldmark v1.4.1 h1:/vn0k+RBvwlxEmP5E7SZMqNxPhfMVFEJiykr15/0XKM= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index 6ef2e7984fa..abef7b44729 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go @@ -24,9 +24,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/internal/checker" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" ) @@ -114,7 +112,7 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns // should match up. for _, act := range r { // file -> message -> edits - fileEdits := make(map[*token.File]map[string][]diff.TextEdit) + fileEdits := make(map[*token.File]map[string][]diff.Edit) fileContents := make(map[*token.File][]byte) // Validate edits, prepare the fileEdits map and read the file contents. @@ -142,17 +140,13 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns } fileContents[file] = contents } - spn, err := span.NewRange(act.Pass.Fset, edit.Pos, edit.End).Span() - if err != nil { - t.Errorf("error converting edit to span %s: %v", file.Name(), err) - } - if _, ok := fileEdits[file]; !ok { - fileEdits[file] = make(map[string][]diff.TextEdit) + fileEdits[file] = make(map[string][]diff.Edit) } - fileEdits[file][sf.Message] = append(fileEdits[file][sf.Message], diff.TextEdit{ - Span: spn, - NewText: string(edit.NewText), + fileEdits[file][sf.Message] = append(fileEdits[file][sf.Message], diff.Edit{ + Start: file.Offset(edit.Pos), + End: file.Offset(edit.End), + New: string(edit.NewText), }) } } @@ -189,7 +183,11 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns for _, vf := range ar.Files { if vf.Name == sf { found = true - out := diff.ApplyEdits(string(orig), edits) + out, err := diff.Apply(string(orig), edits) + if err != nil { + t.Errorf("%s: error applying fixes: %v", file.Name(), err) + continue + } // the file may contain multiple trailing // newlines if the user places empty lines // between files in the archive. normalize @@ -200,12 +198,9 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns t.Errorf("%s: error formatting edited source: %v\n%s", file.Name(), err, out) continue } - if want != string(formatted) { - d, err := myers.ComputeEdits("", want, string(formatted)) - if err != nil { - t.Errorf("failed to compute suggested fix diff: %v", err) - } - t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, d)) + if got := string(formatted); got != want { + unified := diff.Unified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, got) + t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), unified) } break } @@ -217,12 +212,16 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns } else { // all suggested fixes are represented by a single file - var catchallEdits []diff.TextEdit + var catchallEdits []diff.Edit for _, edits := range fixes { catchallEdits = append(catchallEdits, edits...) } - out := diff.ApplyEdits(string(orig), catchallEdits) + out, err := diff.Apply(string(orig), catchallEdits) + if err != nil { + t.Errorf("%s: error applying fixes: %v", file.Name(), err) + continue + } want := string(ar.Comment) formatted, err := format.Source([]byte(out)) @@ -230,12 +229,9 @@ func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns t.Errorf("%s: error formatting resulting source: %v\n%s", file.Name(), err, out) continue } - if want != string(formatted) { - d, err := myers.ComputeEdits("", want, string(formatted)) - if err != nil { - t.Errorf("%s: failed to compute suggested fix diff: %s", file.Name(), err) - } - t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(file.Name()+".golden", "actual", want, d)) + if got := string(formatted); got != want { + unified := diff.Unified(file.Name()+".golden", "actual", want, got) + t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), unified) } } } diff --git a/go/analysis/diagnostic.go b/go/analysis/diagnostic.go index cd462a0cb55..5cdcf46d2a1 100644 --- a/go/analysis/diagnostic.go +++ b/go/analysis/diagnostic.go @@ -37,7 +37,7 @@ type Diagnostic struct { // declaration. type RelatedInformation struct { Pos token.Pos - End token.Pos + End token.Pos // optional Message string } diff --git a/go/analysis/doc.go b/go/analysis/doc.go index 03c31525e36..2c49e335892 100644 --- a/go/analysis/doc.go +++ b/go/analysis/doc.go @@ -177,14 +177,14 @@ Diagnostic is defined as: The optional Category field is a short identifier that classifies the kind of message when an analysis produces several kinds of diagnostic. -Many analyses want to associate diagnostics with a severity level. -Because Diagnostic does not have a severity level field, an Analyzer's -diagnostics effectively all have the same severity level. To separate which -diagnostics are high severity and which are low severity, expose multiple -Analyzers instead. Analyzers should also be separated when their -diagnostics belong in different groups, or could be tagged differently -before being shown to the end user. Analyzers should document their severity -level to help downstream tools surface diagnostics properly. +The Diagnostic struct does not have a field to indicate its severity +because opinions about the relative importance of Analyzers and their +diagnostics vary widely among users. The design of this framework does +not hold each Analyzer responsible for identifying the severity of its +diagnostics. Instead, we expect that drivers will allow the user to +customize the filtering and prioritization of diagnostics based on the +producing Analyzer and optional Category, according to the user's +preferences. Most Analyzers inspect typed Go syntax trees, but a few, such as asmdecl and buildtag, inspect the raw text of Go source files or even non-Go diff --git a/go/analysis/internal/analysisflags/flags.go b/go/analysis/internal/analysisflags/flags.go index 4b7be2d1f5f..2ea630608c4 100644 --- a/go/analysis/internal/analysisflags/flags.go +++ b/go/analysis/internal/analysisflags/flags.go @@ -339,9 +339,38 @@ func PrintPlain(fset *token.FileSet, diag analysis.Diagnostic) { } // A JSONTree is a mapping from package ID to analysis name to result. -// Each result is either a jsonError or a list of jsonDiagnostic. +// Each result is either a jsonError or a list of JSONDiagnostic. type JSONTree map[string]map[string]interface{} +// A TextEdit describes the replacement of a portion of a file. +// Start and End are zero-based half-open indices into the original byte +// sequence of the file, and New is the new text. +type JSONTextEdit struct { + Filename string `json:"filename"` + Start int `json:"start"` + End int `json:"end"` + New string `json:"new"` +} + +// A JSONSuggestedFix describes an edit that should be applied as a whole or not +// at all. It might contain multiple TextEdits/text_edits if the SuggestedFix +// consists of multiple non-contiguous edits. +type JSONSuggestedFix struct { + Message string `json:"message"` + Edits []JSONTextEdit `json:"edits"` +} + +// A JSONDiagnostic can be used to encode and decode analysis.Diagnostics to and +// from JSON. +// TODO(matloob): Should the JSON diagnostics contain ranges? +// If so, how should they be formatted? +type JSONDiagnostic struct { + Category string `json:"category,omitempty"` + Posn string `json:"posn"` + Message string `json:"message"` + SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"` +} + // Add adds the result of analysis 'name' on package 'id'. // The result is either a list of diagnostics or an error. func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) { @@ -352,20 +381,31 @@ func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis. } v = jsonError{err.Error()} } else if len(diags) > 0 { - type jsonDiagnostic struct { - Category string `json:"category,omitempty"` - Posn string `json:"posn"` - Message string `json:"message"` - } - var diagnostics []jsonDiagnostic - // TODO(matloob): Should the JSON diagnostics contain ranges? - // If so, how should they be formatted? + diagnostics := make([]JSONDiagnostic, 0, len(diags)) for _, f := range diags { - diagnostics = append(diagnostics, jsonDiagnostic{ - Category: f.Category, - Posn: fset.Position(f.Pos).String(), - Message: f.Message, - }) + var fixes []JSONSuggestedFix + for _, fix := range f.SuggestedFixes { + var edits []JSONTextEdit + for _, edit := range fix.TextEdits { + edits = append(edits, JSONTextEdit{ + Filename: fset.Position(edit.Pos).Filename, + Start: fset.Position(edit.Pos).Offset, + End: fset.Position(edit.End).Offset, + New: string(edit.NewText), + }) + } + fixes = append(fixes, JSONSuggestedFix{ + Message: fix.Message, + Edits: edits, + }) + } + jdiag := JSONDiagnostic{ + Category: f.Category, + Posn: fset.Position(f.Pos).String(), + Message: f.Message, + SuggestedFixes: fixes, + } + diagnostics = append(diagnostics, jdiag) } v = diagnostics } diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index 51cbf689ac0..2972e44155a 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -26,6 +26,7 @@ import ( "runtime/pprof" "runtime/trace" "sort" + "strconv" "strings" "sync" "time" @@ -34,7 +35,6 @@ import ( "golang.org/x/tools/go/analysis/internal/analysisflags" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/span" ) var ( @@ -147,7 +147,11 @@ func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) { roots := analyze(initial, analyzers) if Fix { - applyFixes(roots) + if err := applyFixes(roots); err != nil { + // Fail when applying fixes failed. + log.Print(err) + return 1 + } } return printDiagnostics(roots) } @@ -305,7 +309,7 @@ func analyze(pkgs []*packages.Package, analyzers []*analysis.Analyzer) []*action return roots } -func applyFixes(roots []*action) { +func applyFixes(roots []*action) error { visited := make(map[*action]bool) var apply func(*action) error var visitAll func(actions []*action) error @@ -313,7 +317,9 @@ func applyFixes(roots []*action) { for _, act := range actions { if !visited[act] { visited[act] = true - visitAll(act.deps) + if err := visitAll(act.deps); err != nil { + return err + } if err := apply(act); err != nil { return err } @@ -332,6 +338,10 @@ func applyFixes(roots []*action) { edit offsetedit left, right *node } + // Edits x and y are equivalent. + equiv := func(x, y offsetedit) bool { + return x.start == y.start && x.end == y.end && bytes.Equal(x.newText, y.newText) + } var insert func(tree **node, edit offsetedit) error insert = func(treeptr **node, edit offsetedit) error { @@ -345,6 +355,13 @@ func applyFixes(roots []*action) { } else if edit.start >= tree.edit.end { return insert(&tree.right, edit) } + if equiv(edit, tree.edit) { // equivalent edits? + // We skip over equivalent edits without considering them + // an error. This handles identical edits coming from the + // multiple ways of loading a package into a + // *go/packages.Packages for testing, e.g. packages "p" and "p [p.test]". + return nil + } // Overlapping text edit. return fmt.Errorf("analyses applying overlapping text edits affecting pos range (%v, %v) and (%v, %v)", @@ -384,14 +401,16 @@ func applyFixes(roots []*action) { return nil } - visitAll(roots) + if err := visitAll(roots); err != nil { + return err + } fset := token.NewFileSet() // Shared by parse calls below // Now we've got a set of valid edits for each file. Get the new file contents. for f, tree := range editsForFile { contents, err := ioutil.ReadFile(f.Name()) if err != nil { - log.Fatal(err) + return err } cur := 0 // current position in the file @@ -408,6 +427,8 @@ func applyFixes(roots []*action) { if edit.start > cur { out.Write(contents[cur:edit.start]) out.Write(edit.newText) + } else if cur == 0 && edit.start == 0 { // edit starts at first character? + out.Write(edit.newText) } cur = edit.end @@ -430,8 +451,11 @@ func applyFixes(roots []*action) { } } - ioutil.WriteFile(f.Name(), out.Bytes(), 0644) + if err := ioutil.WriteFile(f.Name(), out.Bytes(), 0644); err != nil { + return err + } } + return nil } // printDiagnostics prints the diagnostics for the root packages in either @@ -578,7 +602,6 @@ type action struct { deps []*action objectFacts map[objectFactKey]analysis.Fact packageFacts map[packageFactKey]analysis.Fact - inputs map[*analysis.Analyzer]interface{} result interface{} diagnostics []analysis.Diagnostic err error @@ -699,16 +722,52 @@ func (act *action) execOnce() { // Get any type errors that are attributed to the pkg. // This is necessary to test analyzers that provide // suggested fixes for compiler/type errors. + // TODO(adonovan): eliminate this hack; + // see https://github.com/golang/go/issues/54619. for _, err := range act.pkg.Errors { if err.Kind != packages.TypeError { continue } - // err.Pos is a string of form: "file:line:col" or "file:line" or "" or "-" - spn := span.Parse(err.Pos) + + // Parse err.Pos, a string of form: "file:line:col" or "file:line" or "" or "-" + // The filename may have a single ASCII letter Windows drive prefix such as "C:" + var file string + var line, col int + var convErr error + words := strings.Split(err.Pos, ":") + if runtime.GOOS == "windows" && + len(words) > 2 && + len(words[0]) == 1 && + ('A' <= words[0][0] && words[0][0] <= 'Z' || + 'a' <= words[0][0] && words[0][0] <= 'z') { + words[1] = words[0] + ":" + words[1] + words = words[1:] + } + switch len(words) { + case 2: + // file:line + file = words[0] + line, convErr = strconv.Atoi(words[1]) + case 3: + // file:line:col + file = words[0] + line, convErr = strconv.Atoi(words[1]) + if convErr == nil { + col, convErr = strconv.Atoi(words[2]) + } + default: + continue + } + if convErr != nil { + continue + } + // Extract the token positions from the error string. - line, col, offset := spn.Start().Line(), spn.Start().Column(), -1 + // (This is guesswork: Fset may contain all manner + // of stale files with the same name.) + offset := -1 act.pkg.Fset.Iterate(func(f *token.File) bool { - if f.Name() != spn.URI().Filename() { + if f.Name() != file { return true } offset = int(f.LineStart(line)) + col - 1 @@ -766,7 +825,7 @@ func inheritFacts(act, dep *action) { if serialize { encodedFact, err := codeFact(fact) if err != nil { - log.Panicf("internal error: encoding of %T fact failed in %v", fact, act) + log.Panicf("internal error: encoding of %T fact failed in %v: %v", fact, act, err) } fact = encodedFact } @@ -894,7 +953,7 @@ func (act *action) exportObjectFact(obj types.Object, fact analysis.Fact) { func (act *action) allObjectFacts() []analysis.ObjectFact { facts := make([]analysis.ObjectFact, 0, len(act.objectFacts)) for k := range act.objectFacts { - facts = append(facts, analysis.ObjectFact{k.obj, act.objectFacts[k]}) + facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: act.objectFacts[k]}) } return facts } @@ -940,7 +999,7 @@ func factType(fact analysis.Fact) reflect.Type { func (act *action) allPackageFacts() []analysis.PackageFact { facts := make([]analysis.PackageFact, 0, len(act.packageFacts)) for k := range act.packageFacts { - facts = append(facts, analysis.PackageFact{k.pkg, act.packageFacts[k]}) + facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: act.packageFacts[k]}) } return facts } diff --git a/go/analysis/internal/checker/checker_test.go b/go/analysis/internal/checker/checker_test.go index eee211c21a4..f07963fa008 100644 --- a/go/analysis/internal/checker/checker_test.go +++ b/go/analysis/internal/checker/checker_test.go @@ -19,14 +19,9 @@ import ( "golang.org/x/tools/internal/testenv" ) -var from, to string - func TestApplyFixes(t *testing.T) { testenv.NeedsGoPackages(t) - from = "bar" - to = "baz" - files := map[string]string{ "rename/test.go": `package rename @@ -75,6 +70,10 @@ var analyzer = &analysis.Analyzer{ } func run(pass *analysis.Pass) (interface{}, error) { + const ( + from = "bar" + to = "baz" + ) inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{(*ast.Ident)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { @@ -129,6 +128,18 @@ func Foo(s string) int { RunDespiteErrors: true, } + // A no-op analyzer that should finish regardless of + // parse or type errors in the code. + noopWithFact := &analysis.Analyzer{ + Name: "noopfact", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: func(pass *analysis.Pass) (interface{}, error) { + return nil, nil + }, + RunDespiteErrors: true, + FactTypes: []analysis.Fact{&EmptyFact{}}, + } + for _, test := range []struct { name string pattern []string @@ -137,7 +148,17 @@ func Foo(s string) int { }{ // parse/type errors {name: "skip-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{analyzer}, code: 1}, - {name: "despite-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noop}, code: 0}, + // RunDespiteErrors allows a driver to run an Analyzer even after parse/type errors. + // + // The noop analyzer doesn't use facts, so the driver loads only the root + // package from source. For the rest, it asks 'go list' for export data, + // which fails because the compiler encounters the type error. Since the + // errors come from 'go list', the driver doesn't run the analyzer. + {name: "despite-error", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noop}, code: 1}, + // The noopfact analyzer does use facts, so the driver loads source for + // all dependencies, does type checking itself, recognizes the error as a + // type error, and runs the analyzer. + {name: "despite-error-fact", pattern: []string{"file=" + path}, analyzers: []*analysis.Analyzer{noopWithFact}, code: 0}, // combination of parse/type errors and no errors {name: "despite-error-and-no-error", pattern: []string{"file=" + path, "sort"}, analyzers: []*analysis.Analyzer{analyzer, noop}, code: 1}, // non-existing package error @@ -151,6 +172,10 @@ func Foo(s string) int { // no errors {name: "no-errors", pattern: []string{"sort"}, analyzers: []*analysis.Analyzer{analyzer, noop}, code: 0}, } { + if test.name == "despite-error" && testenv.Go1Point() < 20 { + // The behavior in the comment on the despite-error test only occurs for Go 1.20+. + continue + } if got := checker.Run(test.pattern, test.analyzers); got != test.code { t.Errorf("got incorrect exit code %d for test %s; want %d", got, test.name, test.code) } @@ -158,3 +183,7 @@ func Foo(s string) int { defer cleanup() } + +type EmptyFact struct{} + +func (f *EmptyFact) AFact() {} diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go new file mode 100644 index 00000000000..a114c01b645 --- /dev/null +++ b/go/analysis/internal/checker/fix_test.go @@ -0,0 +1,143 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package checker_test + +import ( + "flag" + "io/ioutil" + "os" + "os/exec" + "path" + "runtime" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/internal/checker" + "golang.org/x/tools/internal/testenv" +) + +func main() { + checker.Fix = true + patterns := flag.Args() + + code := checker.Run(patterns, []*analysis.Analyzer{analyzer}) + os.Exit(code) +} + +// TestFixes ensures that checker.Run applies fixes correctly. +// This test fork/execs the main function above. +func TestFixes(t *testing.T) { + oses := map[string]bool{"darwin": true, "linux": true} + if !oses[runtime.GOOS] { + t.Skipf("skipping fork/exec test on this platform") + } + + if os.Getenv("TESTFIXES_CHILD") == "1" { + // child process + + // replace [progname -test.run=TestFixes -- ...] + // by [progname ...] + os.Args = os.Args[2:] + os.Args[0] = "vet" + main() + panic("unreachable") + } + + testenv.NeedsTool(t, "go") + + files := map[string]string{ + "rename/foo.go": `package rename + +func Foo() { + bar := 12 + _ = bar +} + +// the end +`, + "rename/intestfile_test.go": `package rename + +func InTestFile() { + bar := 13 + _ = bar +} + +// the end +`, + "rename/foo_test.go": `package rename_test + +func Foo() { + bar := 14 + _ = bar +} + +// the end +`, + } + fixed := map[string]string{ + "rename/foo.go": `package rename + +func Foo() { + baz := 12 + _ = baz +} + +// the end +`, + "rename/intestfile_test.go": `package rename + +func InTestFile() { + baz := 13 + _ = baz +} + +// the end +`, + "rename/foo_test.go": `package rename_test + +func Foo() { + baz := 14 + _ = baz +} + +// the end +`, + } + dir, cleanup, err := analysistest.WriteFiles(files) + if err != nil { + t.Fatalf("Creating test files failed with %s", err) + } + defer cleanup() + + args := []string{"-test.run=TestFixes", "--", "rename"} + cmd := exec.Command(os.Args[0], args...) + cmd.Env = append(os.Environ(), "TESTFIXES_CHILD=1", "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off") + + out, err := cmd.CombinedOutput() + if len(out) > 0 { + t.Logf("%s: out=<<%s>>", args, out) + } + var exitcode int + if err, ok := err.(*exec.ExitError); ok { + exitcode = err.ExitCode() // requires go1.12 + } + + const diagnosticsExitCode = 3 + if exitcode != diagnosticsExitCode { + t.Errorf("%s: exited %d, want %d", args, exitcode, diagnosticsExitCode) + } + + for name, want := range fixed { + path := path.Join(dir, "src", name) + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Errorf("error reading %s: %v", path, err) + } + if got := string(contents); got != want { + t.Errorf("contents of %s file did not match expectations. got=%s, want=%s", path, got, want) + } + } +} diff --git a/go/analysis/internal/checker/start_test.go b/go/analysis/internal/checker/start_test.go new file mode 100644 index 00000000000..ede21159bc8 --- /dev/null +++ b/go/analysis/internal/checker/start_test.go @@ -0,0 +1,85 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package checker_test + +import ( + "go/ast" + "io/ioutil" + "path/filepath" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/internal/checker" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/testenv" +) + +// TestStartFixes make sure modifying the first character +// of the file takes effect. +func TestStartFixes(t *testing.T) { + testenv.NeedsGoPackages(t) + + files := map[string]string{ + "comment/doc.go": `/* Package comment */ +package comment +`} + + want := `// Package comment +package comment +` + + testdata, cleanup, err := analysistest.WriteFiles(files) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(testdata, "src/comment/doc.go") + checker.Fix = true + checker.Run([]string{"file=" + path}, []*analysis.Analyzer{commentAnalyzer}) + + contents, err := ioutil.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + got := string(contents) + if got != want { + t.Errorf("contents of rewritten file\ngot: %s\nwant: %s", got, want) + } + + defer cleanup() +} + +var commentAnalyzer = &analysis.Analyzer{ + Name: "comment", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: commentRun, +} + +func commentRun(pass *analysis.Pass) (interface{}, error) { + const ( + from = "/* Package comment */" + to = "// Package comment" + ) + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + inspect.Preorder(nil, func(n ast.Node) { + if n, ok := n.(*ast.Comment); ok && n.Text == from { + pass.Report(analysis.Diagnostic{ + Pos: n.Pos(), + End: n.End(), + SuggestedFixes: []analysis.SuggestedFix{{ + TextEdits: []analysis.TextEdit{{ + Pos: n.Pos(), + End: n.End(), + NewText: []byte(to), + }}, + }}, + }) + } + }) + + return nil, nil +} diff --git a/go/analysis/passes/asmdecl/asmdecl.go b/go/analysis/passes/asmdecl/asmdecl.go index 6fbfe7e181c..7288559fc0e 100644 --- a/go/analysis/passes/asmdecl/asmdecl.go +++ b/go/analysis/passes/asmdecl/asmdecl.go @@ -92,7 +92,7 @@ var ( asmArchMips64LE = asmArch{name: "mips64le", bigEndian: false, stack: "R29", lr: true} asmArchPpc64 = asmArch{name: "ppc64", bigEndian: true, stack: "R1", lr: true, retRegs: []string{"R3", "F1"}} asmArchPpc64LE = asmArch{name: "ppc64le", bigEndian: false, stack: "R1", lr: true, retRegs: []string{"R3", "F1"}} - asmArchRISCV64 = asmArch{name: "riscv64", bigEndian: false, stack: "SP", lr: true} + asmArchRISCV64 = asmArch{name: "riscv64", bigEndian: false, stack: "SP", lr: true, retRegs: []string{"X10", "F10"}} asmArchS390X = asmArch{name: "s390x", bigEndian: true, stack: "R15", lr: true} asmArchWasm = asmArch{name: "wasm", bigEndian: false, stack: "SP", lr: false} diff --git a/go/analysis/passes/asmdecl/asmdecl_test.go b/go/analysis/passes/asmdecl/asmdecl_test.go index f6b01a9c308..50938a07571 100644 --- a/go/analysis/passes/asmdecl/asmdecl_test.go +++ b/go/analysis/passes/asmdecl/asmdecl_test.go @@ -19,11 +19,12 @@ var goosarches = []string{ "linux/arm", // asm3.s // TODO: skip test on loong64 until go toolchain supported loong64. // "linux/loong64", // asm10.s - "linux/mips64", // asm5.s - "linux/s390x", // asm6.s - "linux/ppc64", // asm7.s - "linux/mips", // asm8.s, - "js/wasm", // asm9.s + "linux/mips64", // asm5.s + "linux/s390x", // asm6.s + "linux/ppc64", // asm7.s + "linux/mips", // asm8.s, + "js/wasm", // asm9.s + "linux/riscv64", // asm11.s } func Test(t *testing.T) { diff --git a/go/analysis/passes/asmdecl/testdata/src/a/asm11.s b/go/analysis/passes/asmdecl/testdata/src/a/asm11.s new file mode 100644 index 00000000000..e81e8ee179f --- /dev/null +++ b/go/analysis/passes/asmdecl/testdata/src/a/asm11.s @@ -0,0 +1,13 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build riscv64 + +// writing to result in ABIInternal function +TEXT ·returnABIInternal(SB), NOSPLIT, $8 + MOV $123, X10 + RET +TEXT ·returnmissingABIInternal(SB), NOSPLIT, $8 + MOV $123, X20 + RET // want `RET without writing to result register` diff --git a/go/analysis/passes/assign/assign.go b/go/analysis/passes/assign/assign.go index 3586638efc0..89146b73346 100644 --- a/go/analysis/passes/assign/assign.go +++ b/go/analysis/passes/assign/assign.go @@ -12,6 +12,7 @@ import ( "fmt" "go/ast" "go/token" + "go/types" "reflect" "golang.org/x/tools/go/analysis" @@ -51,7 +52,8 @@ func run(pass *analysis.Pass) (interface{}, error) { for i, lhs := range stmt.Lhs { rhs := stmt.Rhs[i] if analysisutil.HasSideEffects(pass.TypesInfo, lhs) || - analysisutil.HasSideEffects(pass.TypesInfo, rhs) { + analysisutil.HasSideEffects(pass.TypesInfo, rhs) || + isMapIndex(pass.TypesInfo, lhs) { continue // expressions may not be equal } if reflect.TypeOf(lhs) != reflect.TypeOf(rhs) { @@ -74,3 +76,14 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } + +// isMapIndex returns true if e is a map index expression. +func isMapIndex(info *types.Info, e ast.Expr) bool { + if idx, ok := analysisutil.Unparen(e).(*ast.IndexExpr); ok { + if typ := info.Types[idx.X].Type; typ != nil { + _, ok := typ.Underlying().(*types.Map) + return ok + } + } + return false +} diff --git a/go/analysis/passes/assign/testdata/src/a/a.go b/go/analysis/passes/assign/testdata/src/a/a.go index eaec634d181..f9663120b4a 100644 --- a/go/analysis/passes/assign/testdata/src/a/a.go +++ b/go/analysis/passes/assign/testdata/src/a/a.go @@ -29,3 +29,31 @@ func (s *ST) SetX(x int, ch chan int) { } func num() int { return 2 } + +func Index() { + s := []int{1} + s[0] = s[0] // want "self-assignment" + + var a [5]int + a[0] = a[0] // want "self-assignment" + + pa := &[2]int{1, 2} + pa[1] = pa[1] // want "self-assignment" + + var pss *struct { // report self assignment despite nil dereference + s []int + } + pss.s[0] = pss.s[0] // want "self-assignment" + + m := map[int]string{1: "a"} + m[0] = m[0] // bail on map self-assignments due to side effects + m[1] = m[1] // not modeling what elements must be in the map + (m[2]) = (m[2]) // even with parens + type Map map[string]bool + named := make(Map) + named["s"] = named["s"] // even on named maps. + var psm *struct { + m map[string]int + } + psm.m["key"] = psm.m["key"] // handles dereferences +} diff --git a/go/analysis/passes/assign/testdata/src/a/a.go.golden b/go/analysis/passes/assign/testdata/src/a/a.go.golden index 6c91d3666cc..f45b7f208e2 100644 --- a/go/analysis/passes/assign/testdata/src/a/a.go.golden +++ b/go/analysis/passes/assign/testdata/src/a/a.go.golden @@ -29,3 +29,31 @@ func (s *ST) SetX(x int, ch chan int) { } func num() int { return 2 } + +func Index() { + s := []int{1} + // want "self-assignment" + + var a [5]int + // want "self-assignment" + + pa := &[2]int{1, 2} + // want "self-assignment" + + var pss *struct { // report self assignment despite nil dereference + s []int + } + // want "self-assignment" + + m := map[int]string{1: "a"} + m[0] = m[0] // bail on map self-assignments due to side effects + m[1] = m[1] // not modeling what elements must be in the map + (m[2]) = (m[2]) // even with parens + type Map map[string]bool + named := make(Map) + named["s"] = named["s"] // even on named maps. + var psm *struct { + m map[string]int + } + psm.m["key"] = psm.m["key"] // handles dereferences +} diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go index d3670aca97a..64e184d3439 100644 --- a/go/analysis/passes/composite/composite.go +++ b/go/analysis/passes/composite/composite.go @@ -7,6 +7,7 @@ package composite import ( + "fmt" "go/ast" "go/types" "strings" @@ -83,7 +84,8 @@ func run(pass *analysis.Pass) (interface{}, error) { } for _, typ := range structuralTypes { under := deref(typ.Underlying()) - if _, ok := under.(*types.Struct); !ok { + strct, ok := under.(*types.Struct) + if !ok { // skip non-struct composite literals continue } @@ -92,20 +94,47 @@ func run(pass *analysis.Pass) (interface{}, error) { continue } - // check if the CompositeLit contains an unkeyed field + // check if the struct contains an unkeyed field allKeyValue := true - for _, e := range cl.Elts { + var suggestedFixAvailable = len(cl.Elts) == strct.NumFields() + var missingKeys []analysis.TextEdit + for i, e := range cl.Elts { if _, ok := e.(*ast.KeyValueExpr); !ok { allKeyValue = false - break + if i >= strct.NumFields() { + break + } + field := strct.Field(i) + if !field.Exported() { + // Adding unexported field names for structs not defined + // locally will not work. + suggestedFixAvailable = false + break + } + missingKeys = append(missingKeys, analysis.TextEdit{ + Pos: e.Pos(), + End: e.Pos(), + NewText: []byte(fmt.Sprintf("%s: ", field.Name())), + }) } } if allKeyValue { - // all the composite literal fields are keyed + // all the struct fields are keyed continue } - pass.ReportRangef(cl, "%s composite literal uses unkeyed fields", typeName) + diag := analysis.Diagnostic{ + Pos: cl.Pos(), + End: cl.End(), + Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName), + } + if suggestedFixAvailable { + diag.SuggestedFixes = []analysis.SuggestedFix{{ + Message: "Add field names to struct literal", + TextEdits: missingKeys, + }} + } + pass.Report(diag) return } }) diff --git a/go/analysis/passes/composite/composite_test.go b/go/analysis/passes/composite/composite_test.go index 952de8bfdad..7afaaa7ffd4 100644 --- a/go/analysis/passes/composite/composite_test.go +++ b/go/analysis/passes/composite/composite_test.go @@ -18,5 +18,5 @@ func Test(t *testing.T) { if typeparams.Enabled { pkgs = append(pkgs, "typeparams") } - analysistest.Run(t, testdata, composite.Analyzer, pkgs...) + analysistest.RunWithSuggestedFixes(t, testdata, composite.Analyzer, pkgs...) } diff --git a/go/analysis/passes/composite/testdata/src/a/a.go b/go/analysis/passes/composite/testdata/src/a/a.go index 3a5bc203b03..cd69d395173 100644 --- a/go/analysis/passes/composite/testdata/src/a/a.go +++ b/go/analysis/passes/composite/testdata/src/a/a.go @@ -11,6 +11,7 @@ import ( "go/scanner" "go/token" "image" + "sync" "unicode" ) @@ -79,6 +80,18 @@ var badStructLiteral = flag.Flag{ // want "unkeyed fields" nil, // Value "DefValue", } +var tooManyFieldsStructLiteral = flag.Flag{ // want "unkeyed fields" + "Name", + "Usage", + nil, // Value + "DefValue", + "Extra Field", +} +var tooFewFieldsStructLiteral = flag.Flag{ // want "unkeyed fields" + "Name", + "Usage", + nil, // Value +} var delta [3]rune @@ -100,6 +113,10 @@ var badScannerErrorList = scanner.ErrorList{ &scanner.Error{token.Position{}, "foobar"}, // want "unkeyed fields" } +// sync.Mutex has unexported fields. We expect a diagnostic but no +// suggested fix. +var mu = sync.Mutex{0, 0} // want "unkeyed fields" + // Check whitelisted structs: if vet is run with --compositewhitelist=false, // this line triggers an error. var whitelistedPoint = image.Point{1, 2} diff --git a/go/analysis/passes/composite/testdata/src/a/a.go.golden b/go/analysis/passes/composite/testdata/src/a/a.go.golden new file mode 100644 index 00000000000..fe73a2e0a1d --- /dev/null +++ b/go/analysis/passes/composite/testdata/src/a/a.go.golden @@ -0,0 +1,144 @@ +// Copyright 2012 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 the test for untagged struct literals. + +package a + +import ( + "flag" + "go/scanner" + "go/token" + "image" + "sync" + "unicode" +) + +var Okay1 = []string{ + "Name", + "Usage", + "DefValue", +} + +var Okay2 = map[string]bool{ + "Name": true, + "Usage": true, + "DefValue": true, +} + +var Okay3 = struct { + X string + Y string + Z string +}{ + "Name", + "Usage", + "DefValue", +} + +var Okay4 = []struct { + A int + B int +}{ + {1, 2}, + {3, 4}, +} + +type MyStruct struct { + X string + Y string + Z string +} + +var Okay5 = &MyStruct{ + "Name", + "Usage", + "DefValue", +} + +var Okay6 = []MyStruct{ + {"foo", "bar", "baz"}, + {"aa", "bb", "cc"}, +} + +var Okay7 = []*MyStruct{ + {"foo", "bar", "baz"}, + {"aa", "bb", "cc"}, +} + +// Testing is awkward because we need to reference things from a separate package +// to trigger the warnings. + +var goodStructLiteral = flag.Flag{ + Name: "Name", + Usage: "Usage", +} +var badStructLiteral = flag.Flag{ // want "unkeyed fields" + Name: "Name", + Usage: "Usage", + Value: nil, // Value + DefValue: "DefValue", +} +var tooManyFieldsStructLiteral = flag.Flag{ // want "unkeyed fields" + "Name", + "Usage", + nil, // Value + "DefValue", + "Extra Field", +} +var tooFewFieldsStructLiteral = flag.Flag{ // want "unkeyed fields" + "Name", + "Usage", + nil, // Value +} + +var delta [3]rune + +// SpecialCase is a named slice of CaseRange to test issue 9171. +var goodNamedSliceLiteral = unicode.SpecialCase{ + {Lo: 1, Hi: 2, Delta: delta}, + unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta}, +} +var badNamedSliceLiteral = unicode.SpecialCase{ + {Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields" + unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields" +} + +// ErrorList is a named slice, so no warnings should be emitted. +var goodScannerErrorList = scanner.ErrorList{ + &scanner.Error{Msg: "foobar"}, +} +var badScannerErrorList = scanner.ErrorList{ + &scanner.Error{Pos: token.Position{}, Msg: "foobar"}, // want "unkeyed fields" +} + +// sync.Mutex has unexported fields. We expect a diagnostic but no +// suggested fix. +var mu = sync.Mutex{0, 0} // want "unkeyed fields" + +// Check whitelisted structs: if vet is run with --compositewhitelist=false, +// this line triggers an error. +var whitelistedPoint = image.Point{1, 2} + +// Do not check type from unknown package. +// See issue 15408. +var unknownPkgVar = unicode.NoSuchType{"foo", "bar"} + +// A named pointer slice of CaseRange to test issue 23539. In +// particular, we're interested in how some slice elements omit their +// type. +var goodNamedPointerSliceLiteral = []*unicode.CaseRange{ + {Lo: 1, Hi: 2}, + &unicode.CaseRange{Lo: 1, Hi: 2}, +} +var badNamedPointerSliceLiteral = []*unicode.CaseRange{ + {Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields" + &unicode.CaseRange{Lo: 1, Hi: 2, Delta: delta}, // want "unkeyed fields" +} + +// unicode.Range16 is whitelisted, so there'll be no vet error +var range16 = unicode.Range16{0xfdd0, 0xfdef, 1} + +// unicode.Range32 is whitelisted, so there'll be no vet error +var range32 = unicode.Range32{0x1fffe, 0x1ffff, 1} diff --git a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden new file mode 100644 index 00000000000..20b652e88dd --- /dev/null +++ b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden @@ -0,0 +1,16 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package a + +import "testing" + +var fuzzTargets = []testing.InternalFuzzTarget{ + {"Fuzz", Fuzz}, +} + +func Fuzz(f *testing.F) {} diff --git a/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go index dd5d57efed4..f9a5e1fb105 100644 --- a/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go @@ -6,7 +6,7 @@ package typeparams import "typeparams/lib" -type localStruct struct { F int } +type localStruct struct{ F int } func F[ T1 ~struct{ f int }, @@ -20,8 +20,8 @@ func F[ _ = T1{2} _ = T2a{2} _ = T2b{2} // want "unkeyed fields" - _ = T3{1,2} - _ = T4{1,2} - _ = T5{1:2} - _ = T6{1:2} + _ = T3{1, 2} + _ = T4{1, 2} + _ = T5{1: 2} + _ = T6{1: 2} } diff --git a/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go.golden b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go.golden new file mode 100644 index 00000000000..66cd9158cb6 --- /dev/null +++ b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go.golden @@ -0,0 +1,27 @@ +// 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 typeparams + +import "typeparams/lib" + +type localStruct struct{ F int } + +func F[ + T1 ~struct{ f int }, + T2a localStruct, + T2b lib.Struct, + T3 ~[]int, + T4 lib.Slice, + T5 ~map[int]int, + T6 lib.Map, +]() { + _ = T1{2} + _ = T2a{2} + _ = T2b{F: 2} // want "unkeyed fields" + _ = T3{1, 2} + _ = T4{1, 2} + _ = T5{1: 2} + _ = T6{1: 2} +} diff --git a/go/analysis/passes/copylock/testdata/src/a/copylock.go b/go/analysis/passes/copylock/testdata/src/a/copylock.go index 7704b3a42b2..4ab66dca1f6 100644 --- a/go/analysis/passes/copylock/testdata/src/a/copylock.go +++ b/go/analysis/passes/copylock/testdata/src/a/copylock.go @@ -50,27 +50,27 @@ func BadFunc() { var t Tlock var tp *Tlock tp = &t - *tp = t // want `assignment copies lock value to \*tp: a.Tlock contains sync.Once contains sync.Mutex` - t = *tp // want "assignment copies lock value to t: a.Tlock contains sync.Once contains sync.Mutex" + *tp = t // want `assignment copies lock value to \*tp: a.Tlock contains sync.Once contains sync\b.*` + t = *tp // want `assignment copies lock value to t: a.Tlock contains sync.Once contains sync\b.*` y := *x // want "assignment copies lock value to y: sync.Mutex" - var z = t // want "variable declaration copies lock value to z: a.Tlock contains sync.Once contains sync.Mutex" + var z = t // want `variable declaration copies lock value to z: a.Tlock contains sync.Once contains sync\b.*` w := struct{ L sync.Mutex }{ L: *x, // want `literal copies lock value from \*x: sync.Mutex` } var q = map[int]Tlock{ - 1: t, // want "literal copies lock value from t: a.Tlock contains sync.Once contains sync.Mutex" - 2: *tp, // want `literal copies lock value from \*tp: a.Tlock contains sync.Once contains sync.Mutex` + 1: t, // want `literal copies lock value from t: a.Tlock contains sync.Once contains sync\b.*` + 2: *tp, // want `literal copies lock value from \*tp: a.Tlock contains sync.Once contains sync\b.*` } yy := []Tlock{ - t, // want "literal copies lock value from t: a.Tlock contains sync.Once contains sync.Mutex" - *tp, // want `literal copies lock value from \*tp: a.Tlock contains sync.Once contains sync.Mutex` + t, // want `literal copies lock value from t: a.Tlock contains sync.Once contains sync\b.*` + *tp, // want `literal copies lock value from \*tp: a.Tlock contains sync.Once contains sync\b.*` } // override 'new' keyword new := func(interface{}) {} - new(t) // want "call of new copies lock value: a.Tlock contains sync.Once contains sync.Mutex" + new(t) // want `call of new copies lock value: a.Tlock contains sync.Once contains sync\b.*` // copy of array of locks var muA [5]sync.Mutex @@ -193,9 +193,9 @@ func SyncTypesCheck() { var onceX sync.Once var onceXX = sync.Once{} onceX1 := new(sync.Once) - onceY := onceX // want "assignment copies lock value to onceY: sync.Once contains sync.Mutex" - onceY = onceX // want "assignment copies lock value to onceY: sync.Once contains sync.Mutex" - var onceYY = onceX // want "variable declaration copies lock value to onceYY: sync.Once contains sync.Mutex" + onceY := onceX // want `assignment copies lock value to onceY: sync.Once contains sync\b.*` + onceY = onceX // want `assignment copies lock value to onceY: sync.Once contains sync\b.*` + var onceYY = onceX // want `variable declaration copies lock value to onceYY: sync.Once contains sync\b.*` onceP := &onceX onceZ := &sync.Once{} } diff --git a/go/analysis/passes/copylock/testdata/src/a/copylock_func.go b/go/analysis/passes/copylock/testdata/src/a/copylock_func.go index 801bc6f24f1..0d3168f1ef1 100644 --- a/go/analysis/passes/copylock/testdata/src/a/copylock_func.go +++ b/go/analysis/passes/copylock/testdata/src/a/copylock_func.go @@ -126,7 +126,7 @@ func AcceptedCases() { // sync.Mutex gets called out, but without any reference to the sync.Once. type LocalOnce sync.Once -func (LocalOnce) Bad() {} // want "Bad passes lock by value: a.LocalOnce contains sync.Mutex" +func (LocalOnce) Bad() {} // want `Bad passes lock by value: a.LocalOnce contains sync.\b.*` // False negative: // LocalMutex doesn't have a Lock method. diff --git a/go/analysis/passes/fieldalignment/fieldalignment.go b/go/analysis/passes/fieldalignment/fieldalignment.go index 78afe94ab30..aff663046a3 100644 --- a/go/analysis/passes/fieldalignment/fieldalignment.go +++ b/go/analysis/passes/fieldalignment/fieldalignment.go @@ -23,7 +23,7 @@ import ( const Doc = `find structs that would use less memory if their fields were sorted This analyzer find structs that can be rearranged to use less memory, and provides -a suggested edit with the optimal order. +a suggested edit with the most compact order. Note that there are two different diagnostics reported. One checks struct size, and the other reports "pointer bytes" used. Pointer bytes is how many bytes of the @@ -41,6 +41,11 @@ has 24 pointer bytes because it has to scan further through the *uint32. struct { string; uint32 } has 8 because it can stop immediately after the string pointer. + +Be aware that the most compact order is not always the most efficient. +In rare cases it may cause two variables each updated by its own goroutine +to occupy the same CPU cache line, inducing a form of memory contention +known as "false sharing" that slows down both goroutines. ` var Analyzer = &analysis.Analyzer{ diff --git a/go/analysis/passes/inspect/inspect.go b/go/analysis/passes/inspect/inspect.go index c1c1127d089..165c70cbd36 100644 --- a/go/analysis/passes/inspect/inspect.go +++ b/go/analysis/passes/inspect/inspect.go @@ -24,7 +24,7 @@ // inspect.Preorder(nil, func(n ast.Node) { // ... // }) -// return nil +// return nil, nil // } package inspect diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 98de9a9bacd..bb0715c02b5 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -18,11 +18,16 @@ import ( const Doc = `check references to loop variables from within nested functions -This analyzer checks for references to loop variables from within a -function literal inside the loop body. It checks only instances where -the function literal is called in a defer or go statement that is the -last statement in the loop body, as otherwise we would need whole -program analysis. +This analyzer checks for references to loop variables from within a function +literal inside the loop body. It checks for patterns where access to a loop +variable is known to escape the current loop iteration: + 1. a call to go or defer at the end of the loop body + 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body + 3. a call testing.T.Run where the subtest body invokes t.Parallel() + +In the case of (1) and (2), the analyzer only considers references in the last +statement of the loop body as it is not deep enough to understand the effects +of subsequent statements which might render the reference benign. For example: @@ -50,10 +55,12 @@ func run(pass *analysis.Pass) (interface{}, error) { } inspect.Preorder(nodeFilter, func(n ast.Node) { // Find the variables updated by the loop statement. - var vars []*ast.Ident + var vars []types.Object addVar := func(expr ast.Expr) { - if id, ok := expr.(*ast.Ident); ok { - vars = append(vars, id) + if id, _ := expr.(*ast.Ident); id != nil { + if obj := pass.TypesInfo.ObjectOf(id); obj != nil { + vars = append(vars, obj) + } } } var body *ast.BlockStmt @@ -79,52 +86,79 @@ func run(pass *analysis.Pass) (interface{}, error) { return } - // Inspect a go or defer statement - // if it's the last one in the loop body. - // (We give up if there are following statements, - // because it's hard to prove go isn't followed by wait, - // or defer by return.) - if len(body.List) == 0 { - return - } - // The function invoked in the last return statement. - var fun ast.Expr - switch s := body.List[len(body.List)-1].(type) { - case *ast.GoStmt: - fun = s.Call.Fun - case *ast.DeferStmt: - fun = s.Call.Fun - case *ast.ExprStmt: // check for errgroup.Group.Go() - if call, ok := s.X.(*ast.CallExpr); ok { - fun = goInvokes(pass.TypesInfo, call) - } - } - lit, ok := fun.(*ast.FuncLit) - if !ok { - return - } - ast.Inspect(lit.Body, func(n ast.Node) bool { - id, ok := n.(*ast.Ident) - if !ok || id.Obj == nil { - return true - } - if pass.TypesInfo.Types[id].Type == nil { - // Not referring to a variable (e.g. struct field name) - return true - } - for _, v := range vars { - if v.Obj == id.Obj { - pass.ReportRangef(id, "loop variable %s captured by func literal", - id.Name) + // Inspect statements to find function literals that may be run outside of + // the current loop iteration. + // + // For go, defer, and errgroup.Group.Go, we ignore all but the last + // statement, because it's hard to prove go isn't followed by wait, or + // defer by return. + // + // We consider every t.Run statement in the loop body, because there is + // no such commonly used mechanism for synchronizing parallel subtests. + // It is of course theoretically possible to synchronize parallel subtests, + // though such a pattern is likely to be exceedingly rare as it would be + // fighting against the test runner. + lastStmt := len(body.List) - 1 + for i, s := range body.List { + var stmts []ast.Stmt // statements that must be checked for escaping references + switch s := s.(type) { + case *ast.GoStmt: + if i == lastStmt { + stmts = litStmts(s.Call.Fun) + } + + case *ast.DeferStmt: + if i == lastStmt { + stmts = litStmts(s.Call.Fun) + } + + case *ast.ExprStmt: // check for errgroup.Group.Go and testing.T.Run (with T.Parallel) + if call, ok := s.X.(*ast.CallExpr); ok { + if i == lastStmt { + stmts = litStmts(goInvoke(pass.TypesInfo, call)) + } + if stmts == nil { + stmts = parallelSubtest(pass.TypesInfo, call) + } } } - return true - }) + + for _, stmt := range stmts { + ast.Inspect(stmt, func(n ast.Node) bool { + id, ok := n.(*ast.Ident) + if !ok { + return true + } + obj := pass.TypesInfo.Uses[id] + if obj == nil { + return true + } + for _, v := range vars { + if v == obj { + pass.ReportRangef(id, "loop variable %s captured by func literal", id.Name) + } + } + return true + }) + } + } }) return nil, nil } -// goInvokes returns a function expression that would be called asynchronously +// litStmts returns all statements from the function body of a function +// literal. +// +// If fun is not a function literal, it returns nil. +func litStmts(fun ast.Expr) []ast.Stmt { + lit, _ := fun.(*ast.FuncLit) + if lit == nil { + return nil + } + return lit.Body.List +} + +// goInvoke returns a function expression that would be called asynchronously // (but not awaited) in another goroutine as a consequence of the call. // For example, given the g.Go call below, it returns the function literal expression. // @@ -133,33 +167,164 @@ func run(pass *analysis.Pass) (interface{}, error) { // g.Go(func() error { ... }) // // Currently only "golang.org/x/sync/errgroup.Group()" is considered. -func goInvokes(info *types.Info, call *ast.CallExpr) ast.Expr { - f := typeutil.StaticCallee(info, call) - // Note: Currently only supports: golang.org/x/sync/errgroup.Go. - if f == nil || f.Name() != "Go" { +func goInvoke(info *types.Info, call *ast.CallExpr) ast.Expr { + if !isMethodCall(info, call, "golang.org/x/sync/errgroup", "Group", "Go") { return nil } - recv := f.Type().(*types.Signature).Recv() - if recv == nil { + return call.Args[0] +} + +// parallelSubtest returns statements that can be easily proven to execute +// concurrently via the go test runner, as t.Run has been invoked with a +// function literal that calls t.Parallel. +// +// In practice, users rely on the fact that statements before the call to +// t.Parallel are synchronous. For example by declaring test := test inside the +// function literal, but before the call to t.Parallel. +// +// Therefore, we only flag references in statements that are obviously +// dominated by a call to t.Parallel. As a simple heuristic, we only consider +// statements following the final labeled statement in the function body, to +// avoid scenarios where a jump would cause either the call to t.Parallel or +// the problematic reference to be skipped. +// +// import "testing" +// +// func TestFoo(t *testing.T) { +// tests := []int{0, 1, 2} +// for i, test := range tests { +// t.Run("subtest", func(t *testing.T) { +// println(i, test) // OK +// t.Parallel() +// println(i, test) // Not OK +// }) +// } +// } +func parallelSubtest(info *types.Info, call *ast.CallExpr) []ast.Stmt { + if !isMethodCall(info, call, "testing", "T", "Run") { return nil } - rtype, ok := recv.Type().(*types.Pointer) - if !ok { + + lit, _ := call.Args[1].(*ast.FuncLit) + if lit == nil { return nil } - named, ok := rtype.Elem().(*types.Named) - if !ok { + + // Capture the *testing.T object for the first argument to the function + // literal. + if len(lit.Type.Params.List[0].Names) == 0 { return nil } - if named.Obj().Name() != "Group" { + + tObj := info.Defs[lit.Type.Params.List[0].Names[0]] + if tObj == nil { return nil } + + // Match statements that occur after a call to t.Parallel following the final + // labeled statement in the function body. + // + // We iterate over lit.Body.List to have a simple, fast and "frequent enough" + // dominance relationship for t.Parallel(): lit.Body.List[i] dominates + // lit.Body.List[j] for i < j unless there is a jump. + var stmts []ast.Stmt + afterParallel := false + for _, stmt := range lit.Body.List { + stmt, labeled := unlabel(stmt) + if labeled { + // Reset: naively we don't know if a jump could have caused the + // previously considered statements to be skipped. + stmts = nil + afterParallel = false + } + + if afterParallel { + stmts = append(stmts, stmt) + continue + } + + // Check if stmt is a call to t.Parallel(), for the correct t. + exprStmt, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + expr := exprStmt.X + if isMethodCall(info, expr, "testing", "T", "Parallel") { + call, _ := expr.(*ast.CallExpr) + if call == nil { + continue + } + x, _ := call.Fun.(*ast.SelectorExpr) + if x == nil { + continue + } + id, _ := x.X.(*ast.Ident) + if id == nil { + continue + } + if info.Uses[id] == tObj { + afterParallel = true + } + } + } + + return stmts +} + +// unlabel returns the inner statement for the possibly labeled statement stmt, +// stripping any (possibly nested) *ast.LabeledStmt wrapper. +// +// The second result reports whether stmt was an *ast.LabeledStmt. +func unlabel(stmt ast.Stmt) (ast.Stmt, bool) { + labeled := false + for { + labelStmt, ok := stmt.(*ast.LabeledStmt) + if !ok { + return stmt, labeled + } + labeled = true + stmt = labelStmt.Stmt + } +} + +// isMethodCall reports whether expr is a method call of +// ... +func isMethodCall(info *types.Info, expr ast.Expr, pkgPath, typeName, method string) bool { + call, ok := expr.(*ast.CallExpr) + if !ok { + return false + } + + // Check that we are calling a method + f := typeutil.StaticCallee(info, call) + if f == nil || f.Name() != method { + return false + } + recv := f.Type().(*types.Signature).Recv() + if recv == nil { + return false + } + + // Check that the receiver is a . or + // *.. + rtype := recv.Type() + if ptr, ok := recv.Type().(*types.Pointer); ok { + rtype = ptr.Elem() + } + named, ok := rtype.(*types.Named) + if !ok { + return false + } + if named.Obj().Name() != typeName { + return false + } pkg := f.Pkg() if pkg == nil { - return nil + return false } - if pkg.Path() != "golang.org/x/sync/errgroup" { - return nil + if pkg.Path() != pkgPath { + return false } - return call.Args[0] + + return true } diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index 1498838d7ff..55fb2a4a3d6 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go @@ -5,16 +5,16 @@ package loopclosure_test import ( - "golang.org/x/tools/internal/typeparams" "testing" "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/loopclosure" + "golang.org/x/tools/internal/typeparams" ) func Test(t *testing.T) { testdata := analysistest.TestData() - tests := []string{"a", "golang.org/..."} + tests := []string{"a", "golang.org/...", "subtests"} if typeparams.Enabled { tests = append(tests, "typeparams") } diff --git a/go/analysis/passes/loopclosure/testdata/src/a/a.go b/go/analysis/passes/loopclosure/testdata/src/a/a.go index 2c8e2e6c411..2a17d16bc86 100644 --- a/go/analysis/passes/loopclosure/testdata/src/a/a.go +++ b/go/analysis/passes/loopclosure/testdata/src/a/a.go @@ -6,7 +6,11 @@ package testdata -import "golang.org/x/sync/errgroup" +import ( + "golang.org/x/sync/errgroup" +) + +var A int func _() { var s []int @@ -49,6 +53,19 @@ func _() { println(i, v) }() } + + // iteration variable declared outside the loop + for A = range s { + go func() { + println(A) // want "loop variable A captured by func literal" + }() + } + // iteration variable declared in a different file + for B = range s { + go func() { + println(B) // want "loop variable B captured by func literal" + }() + } // If the key of the range statement is not an identifier // the code should not panic (it used to). var x [2]int @@ -91,9 +108,9 @@ func _() { } } -// Group is used to test that loopclosure does not match on any type named "Group". -// The checker only matches on methods "(*...errgroup.Group).Go". -type Group struct{}; +// Group is used to test that loopclosure only matches Group.Go when Group is +// from the golang.org/x/sync/errgroup package. +type Group struct{} func (g *Group) Go(func() error) {} diff --git a/go/analysis/passes/loopclosure/testdata/src/a/b.go b/go/analysis/passes/loopclosure/testdata/src/a/b.go new file mode 100644 index 00000000000..d4e5da418e5 --- /dev/null +++ b/go/analysis/passes/loopclosure/testdata/src/a/b.go @@ -0,0 +1,9 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testdata + +// B is declared in a separate file to test that object resolution spans the +// entire package. +var B int diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go new file mode 100644 index 00000000000..c95fa1f0b1e --- /dev/null +++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go @@ -0,0 +1,198 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains tests that the loopclosure analyzer detects leaked +// references via parallel subtests. + +package subtests + +import ( + "testing" +) + +// T is used to test that loopclosure only matches T.Run when T is from the +// testing package. +type T struct{} + +// Run should not match testing.T.Run. Note that the second argument is +// intentionally a *testing.T, not a *T, so that we can check both +// testing.T.Parallel inside a T.Run, and a T.Parallel inside a testing.T.Run. +func (t *T) Run(string, func(*testing.T)) { +} + +func (t *T) Parallel() {} + +func _(t *testing.T) { + for i, test := range []int{1, 2, 3} { + // Check that parallel subtests are identified. + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + }) + + // Check that serial tests are OK. + t.Run("", func(t *testing.T) { + println(i) + println(test) + }) + + // Check that the location of t.Parallel matters. + t.Run("", func(t *testing.T) { + println(i) + println(test) + t.Parallel() + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + }) + + // Check that *testing.T value matters. + t.Run("", func(t *testing.T) { + var x testing.T + x.Parallel() + println(i) + println(test) + }) + + // Check that shadowing the loop variables within the test literal is OK if + // it occurs before t.Parallel(). + t.Run("", func(t *testing.T) { + i := i + test := test + t.Parallel() + println(i) + println(test) + }) + + // Check that shadowing the loop variables within the test literal is Not + // OK if it occurs after t.Parallel(). + t.Run("", func(t *testing.T) { + t.Parallel() + i := i // want "loop variable i captured by func literal" + test := test // want "loop variable test captured by func literal" + println(i) // OK + println(test) // OK + }) + + // Check uses in nested blocks. + t.Run("", func(t *testing.T) { + t.Parallel() + { + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + } + }) + + // Check that we catch uses in nested subtests. + t.Run("", func(t *testing.T) { + t.Parallel() + t.Run("", func(t *testing.T) { + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + }) + }) + + // Check that there is no diagnostic if t is not a *testing.T. + t.Run("", func(_ *testing.T) { + t := &T{} + t.Parallel() + println(i) + println(test) + }) + + // Check that there is no diagnostic when a jump to a label may have caused + // the call to t.Parallel to have been skipped. + t.Run("", func(t *testing.T) { + if true { + goto Test + } + t.Parallel() + Test: + println(i) + println(test) + }) + + // Check that there is no diagnostic when a jump to a label may have caused + // the loop variable reference to be skipped, but there is a diagnostic + // when both the call to t.Parallel and the loop variable reference occur + // after the final label in the block. + t.Run("", func(t *testing.T) { + if true { + goto Test + } + t.Parallel() + println(i) // maybe OK + Test: + t.Parallel() + println(test) // want "loop variable test captured by func literal" + }) + + // Check that multiple labels are handled. + t.Run("", func(t *testing.T) { + if true { + goto Test1 + } else { + goto Test2 + } + Test1: + Test2: + t.Parallel() + println(test) // want "loop variable test captured by func literal" + }) + } +} + +// Check that there is no diagnostic when loop variables are shadowed within +// the loop body. +func _(t *testing.T) { + for i, test := range []int{1, 2, 3} { + i := i + test := test + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) + println(test) + }) + } +} + +// Check that t.Run must be *testing.T.Run. +func _(t *T) { + for i, test := range []int{1, 2, 3} { + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) + println(test) + }) + } +} + +// Check that the top-level must be parallel in order to cause a diagnostic. +// +// From https://pkg.go.dev/testing: +// +// "Run does not return until parallel subtests have completed, providing a +// way to clean up after a group of parallel tests" +func _(t *testing.T) { + for _, test := range []int{1, 2, 3} { + // In this subtest, a/b must complete before the synchronous subtest "a" + // completes, so the reference to test does not escape the current loop + // iteration. + t.Run("a", func(s *testing.T) { + s.Run("b", func(u *testing.T) { + u.Parallel() + println(test) + }) + }) + + // In this subtest, c executes concurrently, so the reference to test may + // escape the current loop iteration. + t.Run("c", func(s *testing.T) { + s.Parallel() + s.Run("d", func(u *testing.T) { + println(test) // want "loop variable test captured by func literal" + }) + }) + } +} diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index c4ccc95b4fb..3ac4fcaa28e 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -583,7 +583,6 @@ func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, fn *types.F argNum := firstArg maxArgNum := firstArg anyIndex := false - anyW := false for i, w := 0, 0; i < len(format); i += w { w = 1 if format[i] != '%' { @@ -606,11 +605,6 @@ func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, fn *types.F pass.Reportf(call.Pos(), "%s does not support error-wrapping directive %%w", state.name) return } - if anyW { - pass.Reportf(call.Pos(), "%s call has more than one error-wrapping directive %%w", state.name) - return - } - anyW = true } if len(state.argNums) > 0 { // Continue with the next sequential argument. @@ -672,12 +666,13 @@ func (s *formatState) parseIndex() bool { s.scanNum() ok := true if s.nbytes == len(s.format) || s.nbytes == start || s.format[s.nbytes] != ']' { - ok = false - s.nbytes = strings.Index(s.format, "]") + ok = false // syntax error is either missing "]" or invalid index. + s.nbytes = strings.Index(s.format[start:], "]") if s.nbytes < 0 { s.pass.ReportRangef(s.call, "%s format %s is missing closing ]", s.name, s.format) return false } + s.nbytes = s.nbytes + start } arg32, err := strconv.ParseInt(s.format[start:s.nbytes], 10, 32) if err != nil || !ok || arg32 <= 0 || arg32 > int64(len(s.call.Args)-s.firstArg) { @@ -915,7 +910,7 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, state *formatState) (o if reason != "" { details = " (" + reason + ")" } - pass.ReportRangef(call, "%s format %s has arg %s of wrong type %s%s", state.name, state.format, analysisutil.Format(pass.Fset, arg), typeString, details) + pass.ReportRangef(call, "%s format %s has arg %s of wrong type %s%s, see also https://pkg.go.dev/fmt#hdr-Printing", state.name, state.format, analysisutil.Format(pass.Fset, arg), typeString, details) return false } if v.typ&argString != 0 && v.verb != 'T' && !bytes.Contains(state.flags, []byte{'#'}) { @@ -950,11 +945,16 @@ func recursiveStringer(pass *analysis.Pass, e ast.Expr) (string, bool) { return "", false } + // inScope returns true if e is in the scope of f. + inScope := func(e ast.Expr, f *types.Func) bool { + return f.Scope() != nil && f.Scope().Contains(e.Pos()) + } + // Is the expression e within the body of that String or Error method? var method *types.Func - if strOk && strMethod.Pkg() == pass.Pkg && strMethod.Scope().Contains(e.Pos()) { + if strOk && strMethod.Pkg() == pass.Pkg && inScope(e, strMethod) { method = strMethod - } else if errOk && errMethod.Pkg() == pass.Pkg && errMethod.Scope().Contains(e.Pos()) { + } else if errOk && errMethod.Pkg() == pass.Pkg && inScope(e, errMethod) { method = errMethod } else { return "", false diff --git a/go/analysis/passes/printf/testdata/src/a/a.go b/go/analysis/passes/printf/testdata/src/a/a.go index 5eca3172dec..0c4d11bf0c0 100644 --- a/go/analysis/passes/printf/testdata/src/a/a.go +++ b/go/analysis/passes/printf/testdata/src/a/a.go @@ -217,6 +217,7 @@ func PrintfTests() { Printf("%[2]*.[1]*[3]d x", 2, "hi", 4) // want `a.Printf format %\[2]\*\.\[1\]\*\[3\]d uses non-int \x22hi\x22 as argument of \*` Printf("%[0]s x", "arg1") // want `a.Printf format has invalid argument index \[0\]` Printf("%[0]d x", 1) // want `a.Printf format has invalid argument index \[0\]` + Printf("%[3]*.[2*[1]f", 1, 2, 3) // want `a.Printf format has invalid argument index \[2\*\[1\]` // Something that satisfies the error interface. var e error fmt.Println(e.Error()) // ok @@ -341,7 +342,7 @@ func PrintfTests() { _ = fmt.Errorf("%[2]w %[1]s", "x", err) // OK _ = fmt.Errorf("%[2]w %[1]s", e, "x") // want `fmt.Errorf format %\[2\]w has arg "x" of wrong type string` _ = fmt.Errorf("%w", "x") // want `fmt.Errorf format %w has arg "x" of wrong type string` - _ = fmt.Errorf("%w %w", err, err) // want `fmt.Errorf call has more than one error-wrapping directive %w` + _ = fmt.Errorf("%w %w", err, err) // OK _ = fmt.Errorf("%w", interface{}(nil)) // want `fmt.Errorf format %w has arg interface{}\(nil\) of wrong type interface{}` _ = fmt.Errorf("%w", errorTestOK(0)) // concrete value implements error _ = fmt.Errorf("%w", errSubset) // interface value implements error diff --git a/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go b/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go index 76a9a205a70..c4d7e530d93 100644 --- a/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go +++ b/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go @@ -121,3 +121,25 @@ func TestTermReduction[T1 interface{ ~int | string }, T2 interface { fmt.Printf("%d", t2) fmt.Printf("%s", t2) // want "wrong type.*contains typeparams.myInt" } + +type U[T any] struct{} + +func (u U[T]) String() string { + fmt.Println(u) // want `fmt.Println arg u causes recursive call to \(typeparams.U\[T\]\).String method` + return "" +} + +type S[T comparable] struct { + t T +} + +func (s S[T]) String() T { + fmt.Println(s) // Not flagged. We currently do not consider String() T to implement fmt.Stringer (see #55928). + return s.t +} + +func TestInstanceStringer() { + // Tests String method with nil Scope (#55350) + fmt.Println(&S[string]{}) + fmt.Println(&U[string]{}) +} diff --git a/go/analysis/passes/printf/types.go b/go/analysis/passes/printf/types.go index 270e917c809..7cbb0bdbf5f 100644 --- a/go/analysis/passes/printf/types.go +++ b/go/analysis/passes/printf/types.go @@ -299,13 +299,3 @@ func isConvertibleToString(typ types.Type) bool { return false } - -// hasBasicType reports whether x's type is a types.Basic with the given kind. -func hasBasicType(pass *analysis.Pass, x ast.Expr, kind types.BasicKind) bool { - t := pass.TypesInfo.Types[x].Type - if t != nil { - t = t.Underlying() - } - b, ok := t.(*types.Basic) - return ok && b.Kind() == kind -} diff --git a/go/analysis/passes/sortslice/analyzer.go b/go/analysis/passes/sortslice/analyzer.go index 5eb957a1883..f85837d66bf 100644 --- a/go/analysis/passes/sortslice/analyzer.go +++ b/go/analysis/passes/sortslice/analyzer.go @@ -52,11 +52,20 @@ func run(pass *analysis.Pass) (interface{}, error) { arg := call.Args[0] typ := pass.TypesInfo.Types[arg].Type + + if tuple, ok := typ.(*types.Tuple); ok { + typ = tuple.At(0).Type() // special case for Slice(f(...)) + } + switch typ.Underlying().(type) { case *types.Slice, *types.Interface: return } + // Restore typ to the original type, we may unwrap the tuple above, + // typ might not be the type of arg. + typ = pass.TypesInfo.Types[arg].Type + var fixes []analysis.SuggestedFix switch v := typ.Underlying().(type) { case *types.Array: diff --git a/go/analysis/passes/sortslice/testdata/src/a/a.go b/go/analysis/passes/sortslice/testdata/src/a/a.go index bc6cc16e9f1..c6aca8df13b 100644 --- a/go/analysis/passes/sortslice/testdata/src/a/a.go +++ b/go/analysis/passes/sortslice/testdata/src/a/a.go @@ -6,8 +6,8 @@ import "sort" func IncorrectSort() { i := 5 sortFn := func(i, j int) bool { return false } - sort.Slice(i, sortFn) // want "sort.Slice's argument must be a slice; is called with int" - sort.SliceStable(i, sortFn) // want "sort.SliceStable's argument must be a slice; is called with int" + sort.Slice(i, sortFn) // want "sort.Slice's argument must be a slice; is called with int" + sort.SliceStable(i, sortFn) // want "sort.SliceStable's argument must be a slice; is called with int" sort.SliceIsSorted(i, sortFn) // want "sort.SliceIsSorted's argument must be a slice; is called with int" } @@ -62,3 +62,23 @@ func UnderlyingSlice() { sort.SliceStable(s, sortFn) sort.SliceIsSorted(s, sortFn) } + +// FunctionResultsAsArguments passes a function which returns two values +// that satisfy sort.Slice signature. It should not produce a diagnostic. +func FunctionResultsAsArguments() { + s := []string{"a", "z", "ooo"} + sort.Slice(less(s)) + sort.Slice(lessPtr(s)) // want `sort.Slice's argument must be a slice; is called with \(\*\[\]string,.*` +} + +func less(s []string) ([]string, func(i, j int) bool) { + return s, func(i, j int) bool { + return s[i] < s[j] + } +} + +func lessPtr(s []string) (*[]string, func(i, j int) bool) { + return &s, func(i, j int) bool { + return s[i] < s[j] + } +} diff --git a/go/analysis/passes/stdmethods/stdmethods.go b/go/analysis/passes/stdmethods/stdmethods.go index cc9497179da..41f455d1003 100644 --- a/go/analysis/passes/stdmethods/stdmethods.go +++ b/go/analysis/passes/stdmethods/stdmethods.go @@ -134,6 +134,19 @@ func canonicalMethod(pass *analysis.Pass, id *ast.Ident) { } } + // Special case: Unwrap has two possible signatures. + // Check for Unwrap() []error here. + if id.Name == "Unwrap" { + if args.Len() == 0 && results.Len() == 1 { + t := typeString(results.At(0).Type()) + if t == "error" || t == "[]error" { + return + } + } + pass.ReportRangef(id, "method Unwrap() should have signature Unwrap() error or Unwrap() []error") + return + } + // Do the =s (if any) all match? if !matchParams(pass, expect.args, args, "=") || !matchParams(pass, expect.results, results, "=") { return diff --git a/go/analysis/passes/stdmethods/testdata/src/a/a.go b/go/analysis/passes/stdmethods/testdata/src/a/a.go index c95cf5d2b76..2b01f46932f 100644 --- a/go/analysis/passes/stdmethods/testdata/src/a/a.go +++ b/go/analysis/passes/stdmethods/testdata/src/a/a.go @@ -49,7 +49,7 @@ func (E) Error() string { return "" } // E implements error. func (E) As() {} // want `method As\(\) should have signature As\((any|interface\{\})\) bool` func (E) Is() {} // want `method Is\(\) should have signature Is\(error\) bool` -func (E) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error` +func (E) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error or Unwrap\(\) \[\]error` type F int @@ -57,8 +57,18 @@ func (F) Error() string { return "" } // Both F and *F implement error. func (*F) As() {} // want `method As\(\) should have signature As\((any|interface\{\})\) bool` func (*F) Is() {} // want `method Is\(\) should have signature Is\(error\) bool` -func (*F) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error` +func (*F) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error or Unwrap\(\) \[\]error` type G int func (G) As(interface{}) bool // ok + +type W int + +func (W) Error() string { return "" } +func (W) Unwrap() error { return nil } // ok + +type M int + +func (M) Error() string { return "" } +func (M) Unwrap() []error { return nil } // ok diff --git a/go/analysis/passes/stdmethods/testdata/src/typeparams/typeparams.go b/go/analysis/passes/stdmethods/testdata/src/typeparams/typeparams.go index 72df30d4960..3d4146e9b2c 100644 --- a/go/analysis/passes/stdmethods/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/stdmethods/testdata/src/typeparams/typeparams.go @@ -30,7 +30,7 @@ func (E[_]) Error() string { return "" } // E implements error. func (E[P]) As() {} // want `method As\(\) should have signature As\((any|interface\{\})\) bool` func (E[_]) Is() {} // want `method Is\(\) should have signature Is\(error\) bool` -func (E[_]) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error` +func (E[_]) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error or Unwrap\(\) \[\]error` type F[P any] int @@ -38,4 +38,4 @@ func (F[_]) Error() string { return "" } // Both F and *F implement error. func (*F[_]) As() {} // want `method As\(\) should have signature As\((any|interface\{\})\) bool` func (*F[_]) Is() {} // want `method Is\(\) should have signature Is\(error\) bool` -func (*F[_]) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error` +func (*F[_]) Unwrap() {} // want `method Unwrap\(\) should have signature Unwrap\(\) error or Unwrap\(\) \[\]error` diff --git a/go/analysis/passes/tests/testdata/src/a/go118_test.go b/go/analysis/passes/tests/testdata/src/a/go118_test.go index dc898daca0b..e2bc3f3a0bd 100644 --- a/go/analysis/passes/tests/testdata/src/a/go118_test.go +++ b/go/analysis/passes/tests/testdata/src/a/go118_test.go @@ -94,3 +94,8 @@ func FuzzObjectMethod(f *testing.F) { } f.Fuzz(obj.myVar) // ok } + +// Test for golang/go#56505: checking fuzz arguments should not panic on *error. +func FuzzIssue56505(f *testing.F) { + f.Fuzz(func(e *error) {}) // want "the first parameter of a fuzz target must be \\*testing.T" +} diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go index 56b20ebd519..935aad00c98 100644 --- a/go/analysis/passes/tests/tests.go +++ b/go/analysis/passes/tests/tests.go @@ -269,7 +269,9 @@ func isTestingType(typ types.Type, testingType string) bool { if !ok { return false } - return named.Obj().Pkg().Path() == "testing" && named.Obj().Name() == testingType + obj := named.Obj() + // obj.Pkg is nil for the error type. + return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == testingType } // Validate that fuzz target function's arguments are of accepted types. @@ -475,10 +477,12 @@ func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) { if tparams := typeparams.ForFuncType(fn.Type); tparams != nil && len(tparams.List) > 0 { // Note: cmd/go/internal/load also errors about TestXXX and BenchmarkXXX functions with type parameters. // We have currently decided to also warn before compilation/package loading. This can help users in IDEs. + // TODO(adonovan): use ReportRangef(tparams). pass.Reportf(fn.Pos(), "%s has type parameters: it will not be run by go test as a %sXXX function", fn.Name.Name, prefix) } if !isTestSuffix(fn.Name.Name[len(prefix):]) { + // TODO(adonovan): use ReportRangef(fn.Name). pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) } } diff --git a/go/analysis/passes/timeformat/testdata/src/a/a.go b/go/analysis/passes/timeformat/testdata/src/a/a.go new file mode 100644 index 00000000000..98481446e55 --- /dev/null +++ b/go/analysis/passes/timeformat/testdata/src/a/a.go @@ -0,0 +1,50 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains tests for the timeformat checker. + +package a + +import ( + "time" + + "b" +) + +func hasError() { + a, _ := time.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00") // want `2006-02-01 should be 2006-01-02` + a.Format(`2006-02-01`) // want `2006-02-01 should be 2006-01-02` + a.Format("2006-02-01 15:04:05") // want `2006-02-01 should be 2006-01-02` + + const c = "2006-02-01" + a.Format(c) // want `2006-02-01 should be 2006-01-02` +} + +func notHasError() { + a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00") + a.Format("2006-01-02") + + const c = "2006-01-02" + a.Format(c) + + v := "2006-02-01" + a.Format(v) // Allowed though variables. + + m := map[string]string{ + "y": "2006-02-01", + } + a.Format(m["y"]) + + s := []string{"2006-02-01"} + a.Format(s[0]) + + a.Format(badFormat()) + + o := b.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00") + o.Format("2006-02-01") +} + +func badFormat() string { + return "2006-02-01" +} diff --git a/go/analysis/passes/timeformat/testdata/src/a/a.go.golden b/go/analysis/passes/timeformat/testdata/src/a/a.go.golden new file mode 100644 index 00000000000..9eccded63b4 --- /dev/null +++ b/go/analysis/passes/timeformat/testdata/src/a/a.go.golden @@ -0,0 +1,50 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains tests for the timeformat checker. + +package a + +import ( + "time" + + "b" +) + +func hasError() { + a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00") // want `2006-02-01 should be 2006-01-02` + a.Format(`2006-01-02`) // want `2006-02-01 should be 2006-01-02` + a.Format("2006-01-02 15:04:05") // want `2006-02-01 should be 2006-01-02` + + const c = "2006-02-01" + a.Format(c) // want `2006-02-01 should be 2006-01-02` +} + +func notHasError() { + a, _ := time.Parse("2006-01-02 15:04:05", "2021-01-01 00:00:00") + a.Format("2006-01-02") + + const c = "2006-01-02" + a.Format(c) + + v := "2006-02-01" + a.Format(v) // Allowed though variables. + + m := map[string]string{ + "y": "2006-02-01", + } + a.Format(m["y"]) + + s := []string{"2006-02-01"} + a.Format(s[0]) + + a.Format(badFormat()) + + o := b.Parse("2006-02-01 15:04:05", "2021-01-01 00:00:00") + o.Format("2006-02-01") +} + +func badFormat() string { + return "2006-02-01" +} diff --git a/go/analysis/passes/timeformat/testdata/src/b/b.go b/go/analysis/passes/timeformat/testdata/src/b/b.go new file mode 100644 index 00000000000..de5690863c9 --- /dev/null +++ b/go/analysis/passes/timeformat/testdata/src/b/b.go @@ -0,0 +1,11 @@ +package b + +type B struct { +} + +func Parse(string, string) B { + return B{} +} + +func (b B) Format(string) { +} diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go new file mode 100644 index 00000000000..acb198f95c4 --- /dev/null +++ b/go/analysis/passes/timeformat/timeformat.go @@ -0,0 +1,129 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package timeformat defines an Analyzer that checks for the use +// of time.Format or time.Parse calls with a bad format. +package timeformat + +import ( + "go/ast" + "go/constant" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" +) + +const badFormat = "2006-02-01" +const goodFormat = "2006-01-02" + +const Doc = `check for calls of (time.Time).Format or time.Parse with 2006-02-01 + +The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm) +format. Internationally, "yyyy-dd-mm" does not occur in common calendar date +standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended. +` + +var Analyzer = &analysis.Analyzer{ + Name: "timeformat", + Doc: Doc, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +func run(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), + } + inspect.Preorder(nodeFilter, func(n ast.Node) { + call := n.(*ast.CallExpr) + fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func) + if !ok { + return + } + if !isTimeDotFormat(fn) && !isTimeDotParse(fn) { + return + } + if len(call.Args) > 0 { + arg := call.Args[0] + badAt := badFormatAt(pass.TypesInfo, arg) + + if badAt > -1 { + // Check if it's a literal string, otherwise we can't suggest a fix. + if _, ok := arg.(*ast.BasicLit); ok { + pos := int(arg.Pos()) + badAt + 1 // +1 to skip the " or ` + end := pos + len(badFormat) + + pass.Report(analysis.Diagnostic{ + Pos: token.Pos(pos), + End: token.Pos(end), + Message: badFormat + " should be " + goodFormat, + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Replace " + badFormat + " with " + goodFormat, + TextEdits: []analysis.TextEdit{{ + Pos: token.Pos(pos), + End: token.Pos(end), + NewText: []byte(goodFormat), + }}, + }}, + }) + } else { + pass.Reportf(arg.Pos(), badFormat+" should be "+goodFormat) + } + } + } + }) + return nil, nil +} + +func isTimeDotFormat(f *types.Func) bool { + if f.Name() != "Format" || f.Pkg().Path() != "time" { + return false + } + sig, ok := f.Type().(*types.Signature) + if !ok { + return false + } + // Verify that the receiver is time.Time. + recv := sig.Recv() + if recv == nil { + return false + } + named, ok := recv.Type().(*types.Named) + return ok && named.Obj().Name() == "Time" +} + +func isTimeDotParse(f *types.Func) bool { + if f.Name() != "Parse" || f.Pkg().Path() != "time" { + return false + } + // Verify that there is no receiver. + sig, ok := f.Type().(*types.Signature) + return ok && sig.Recv() == nil +} + +// badFormatAt return the start of a bad format in e or -1 if no bad format is found. +func badFormatAt(info *types.Info, e ast.Expr) int { + tv, ok := info.Types[e] + if !ok { // no type info, assume good + return -1 + } + + t, ok := tv.Type.(*types.Basic) + if !ok || t.Info()&types.IsString == 0 { + return -1 + } + + if tv.Value == nil { + return -1 + } + + return strings.Index(constant.StringVal(tv.Value), badFormat) +} diff --git a/go/analysis/passes/timeformat/timeformat_test.go b/go/analysis/passes/timeformat/timeformat_test.go new file mode 100644 index 00000000000..86bbe1bb3fb --- /dev/null +++ b/go/analysis/passes/timeformat/timeformat_test.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package timeformat_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/timeformat" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.RunWithSuggestedFixes(t, testdata, timeformat.Analyzer, "a") +} diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go index 9827b57f529..d9c8f11cdd4 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go @@ -50,7 +50,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/internal/analysisflags" - "golang.org/x/tools/go/analysis/internal/facts" + "golang.org/x/tools/internal/facts" "golang.org/x/tools/internal/typeparams" ) @@ -287,13 +287,13 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re analyzers = filtered // Read facts from imported packages. - read := func(path string) ([]byte, error) { - if vetx, ok := cfg.PackageVetx[path]; ok { + read := func(imp *types.Package) ([]byte, error) { + if vetx, ok := cfg.PackageVetx[imp.Path()]; ok { return ioutil.ReadFile(vetx) } return nil, nil // no .vetx file, no facts } - facts, err := facts.Decode(pkg, read) + facts, err := facts.NewDecoder(pkg).Decode(read) if err != nil { return nil, err } diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go index 7e5b848de86..197abd9a168 100644 --- a/go/analysis/unitchecker/unitchecker_test.go +++ b/go/analysis/unitchecker/unitchecker_test.go @@ -20,6 +20,7 @@ import ( "strings" "testing" + "golang.org/x/tools/go/analysis/passes/assign" "golang.org/x/tools/go/analysis/passes/findcall" "golang.org/x/tools/go/analysis/passes/printf" "golang.org/x/tools/go/analysis/unitchecker" @@ -41,6 +42,7 @@ func main() { unitchecker.Main( findcall.Analyzer, printf.Analyzer, + assign.Analyzer, ) } @@ -74,6 +76,13 @@ func _() { } func MyFunc123() {} +`, + "c/c.go": `package c + +func _() { + i := 5 + i = i +} `, }}}) defer exported.Cleanup() @@ -84,6 +93,9 @@ func MyFunc123() {} const wantB = `# golang.org/fake/b ([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?b/b.go:6:13: call of MyFunc123\(...\) ([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?b/b.go:7:11: call of MyFunc123\(...\) +` + const wantC = `# golang.org/fake/c +([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go:5:5: self-assignment of i to i ` const wantAJSON = `# golang.org/fake/a \{ @@ -91,23 +103,62 @@ func MyFunc123() {} "findcall": \[ \{ "posn": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?a/a.go:4:11", - "message": "call of MyFunc123\(...\)" + "message": "call of MyFunc123\(...\)", + "suggested_fixes": \[ + \{ + "message": "Add '_TEST_'", + "edits": \[ + \{ + "filename": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?a/a.go", + "start": 32, + "end": 32, + "new": "_TEST_" + \} + \] + \} + \] + \} + \] + \} +\} +` + const wantCJSON = `# golang.org/fake/c +\{ + "golang.org/fake/c": \{ + "assign": \[ + \{ + "posn": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go:5:5", + "message": "self-assignment of i to i", + "suggested_fixes": \[ + \{ + "message": "Remove", + "edits": \[ + \{ + "filename": "([/._\-a-zA-Z0-9]+[\\/]fake[\\/])?c/c.go", + "start": 37, + "end": 42, + "new": "" + \} + \] + \} + \] \} \] \} \} ` - for _, test := range []struct { - args string - wantOut string - wantExit int + args string + wantOut string + wantExitError bool }{ - {args: "golang.org/fake/a", wantOut: wantA, wantExit: 2}, - {args: "golang.org/fake/b", wantOut: wantB, wantExit: 2}, - {args: "golang.org/fake/a golang.org/fake/b", wantOut: wantA + wantB, wantExit: 2}, - {args: "-json golang.org/fake/a", wantOut: wantAJSON, wantExit: 0}, - {args: "-c=0 golang.org/fake/a", wantOut: wantA + "4 MyFunc123\\(\\)\n", wantExit: 2}, + {args: "golang.org/fake/a", wantOut: wantA, wantExitError: true}, + {args: "golang.org/fake/b", wantOut: wantB, wantExitError: true}, + {args: "golang.org/fake/c", wantOut: wantC, wantExitError: true}, + {args: "golang.org/fake/a golang.org/fake/b", wantOut: wantA + wantB, wantExitError: true}, + {args: "-json golang.org/fake/a", wantOut: wantAJSON, wantExitError: false}, + {args: "-json golang.org/fake/c", wantOut: wantCJSON, wantExitError: false}, + {args: "-c=0 golang.org/fake/a", wantOut: wantA + "4 MyFunc123\\(\\)\n", wantExitError: true}, } { cmd := exec.Command("go", "vet", "-vettool="+os.Args[0], "-findcall.name=MyFunc123") cmd.Args = append(cmd.Args, strings.Fields(test.args)...) @@ -119,13 +170,17 @@ func MyFunc123() {} if exitErr, ok := err.(*exec.ExitError); ok { exitcode = exitErr.ExitCode() } - if exitcode != test.wantExit { - t.Errorf("%s: got exit code %d, want %d", test.args, exitcode, test.wantExit) + if (exitcode != 0) != test.wantExitError { + want := "zero" + if test.wantExitError { + want = "nonzero" + } + t.Errorf("%s: got exit code %d, want %s", test.args, exitcode, want) } matched, err := regexp.Match(test.wantOut, out) if err != nil { - t.Fatal(err) + t.Fatalf("regexp.Match(<<%s>>): %v", test.wantOut, err) } if !matched { t.Errorf("%s: got <<%s>>, want match of regexp <<%s>>", test.args, out, test.wantOut) diff --git a/go/ast/inspector/typeof.go b/go/ast/inspector/typeof.go index 11ab2bc85aa..703c8139544 100644 --- a/go/ast/inspector/typeof.go +++ b/go/ast/inspector/typeof.go @@ -11,6 +11,7 @@ package inspector import ( "go/ast" + "math" "golang.org/x/tools/internal/typeparams" ) @@ -218,7 +219,7 @@ func typeOf(n ast.Node) uint64 { func maskOf(nodes []ast.Node) uint64 { if nodes == nil { - return 1<<64 - 1 // match all node types + return math.MaxUint64 // match all node types } var mask uint64 for _, n := range nodes { diff --git a/go/buildutil/util.go b/go/buildutil/util.go index d771b18e32d..bee6390de4c 100644 --- a/go/buildutil/util.go +++ b/go/buildutil/util.go @@ -80,7 +80,7 @@ func ContainingPackage(ctxt *build.Context, dir, filename string) (*build.Packag // (go/build.Context defines these as methods, but does not export them.) -// hasSubdir calls ctxt.HasSubdir (if not nil) or else uses +// HasSubdir calls ctxt.HasSubdir (if not nil) or else uses // the local file system to answer the question. func HasSubdir(ctxt *build.Context, root, dir string) (rel string, ok bool) { if f := ctxt.HasSubdir; f != nil { diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 365d7a5b0f7..2ad0f89fd83 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -568,7 +568,9 @@ func (b *builder) panic(p *ssa.Panic) { func (b *builder) call(c ssa.CallInstruction) { // When c is r := recover() call register instruction, we add Recover -> r. if bf, ok := c.Common().Value.(*ssa.Builtin); ok && bf.Name() == "recover" { - b.addInFlowEdge(recoverReturn{}, b.nodeFromVal(c.(*ssa.Call))) + if v, ok := c.(ssa.Value); ok { + b.addInFlowEdge(recoverReturn{}, b.nodeFromVal(v)) + } return } @@ -586,14 +588,14 @@ func addArgumentFlows(b *builder, c ssa.CallInstruction, f *ssa.Function) { return } cc := c.Common() - // When c is an unresolved method call (cc.Method != nil), cc.Value contains - // the receiver object rather than cc.Args[0]. - if cc.Method != nil { - b.addInFlowAliasEdges(b.nodeFromVal(f.Params[0]), b.nodeFromVal(cc.Value)) - } offset := 0 if cc.Method != nil { + // We don't add interprocedural flows for receiver objects. + // At a call site, the receiver object is interface while the + // callee object is concrete. The flow from interface to + // concrete type does not make sense. The flow other way around + // would bake in information from the initial call graph. offset = 1 } for i, v := range cc.Args { @@ -654,7 +656,7 @@ func (b *builder) addInFlowEdge(s, d node) { // Creates const, pointer, global, func, and local nodes based on register instructions. func (b *builder) nodeFromVal(val ssa.Value) node { - if p, ok := val.Type().(*types.Pointer); ok && !isInterface(p.Elem()) && !isFunction(p.Elem()) { + if p, ok := val.Type().(*types.Pointer); ok && !types.IsInterface(p.Elem()) && !isFunction(p.Elem()) { // Nested pointer to interfaces are modeled as a special // nestedPtrInterface node. if i := interfaceUnderPtr(p.Elem()); i != nil { @@ -687,7 +689,9 @@ func (b *builder) nodeFromVal(val ssa.Value) node { // semantically equivalent types can have different implementations, // this method guarantees the same implementation is always used. func (b *builder) representative(n node) node { - if !hasInitialTypes(n) { + if n.Type() == nil { + // panicArg and recoverReturn do not have + // types and are unique by definition. return n } t := canonicalize(n.Type(), &b.canon) diff --git a/go/callgraph/vta/propagation.go b/go/callgraph/vta/propagation.go index 5934ebc2167..6127780ac4e 100644 --- a/go/callgraph/vta/propagation.go +++ b/go/callgraph/vta/propagation.go @@ -175,6 +175,18 @@ func nodeTypes(nodes []node, builder *trie.Builder, propTypeId func(p propType) return &typeSet } +// hasInitialTypes check if a node can have initial types. +// Returns true iff `n` is not a panic, recover, nestedPtr* +// node, nor a node whose type is an interface. +func hasInitialTypes(n node) bool { + switch n.(type) { + case panicArg, recoverReturn, nestedPtrFunction, nestedPtrInterface: + return false + default: + return !types.IsInterface(n.Type()) + } +} + // getPropType creates a propType for `node` based on its type. // propType.typ is always node.Type(). If node is function, then // propType.val is the underlying function; nil otherwise. diff --git a/go/callgraph/vta/propagation_test.go b/go/callgraph/vta/propagation_test.go index 00b21277f22..f4a754f9663 100644 --- a/go/callgraph/vta/propagation_test.go +++ b/go/callgraph/vta/propagation_test.go @@ -58,7 +58,7 @@ func newLocal(name string, t types.Type) local { // newNamedType creates a bogus type named `name`. func newNamedType(name string) *types.Named { - return types.NewNamed(types.NewTypeName(token.NoPos, nil, name, nil), nil, nil) + return types.NewNamed(types.NewTypeName(token.NoPos, nil, name, nil), types.Universe.Lookup("int").Type(), nil) } // sccString is a utility for stringifying `nodeToScc`. Every diff --git a/go/callgraph/vta/testdata/src/function_alias.go b/go/callgraph/vta/testdata/src/function_alias.go index b38e0e00d69..0a8dffe79d4 100644 --- a/go/callgraph/vta/testdata/src/function_alias.go +++ b/go/callgraph/vta/testdata/src/function_alias.go @@ -33,42 +33,42 @@ func Baz(f func()) { // t2 = *t1 // *t2 = Baz$1 // t3 = local A (a) -// t4 = &t3.foo [#0] -// t5 = *t1 -// t6 = *t5 -// *t4 = t6 +// t4 = *t1 +// t5 = *t4 +// t6 = &t3.foo [#0] +// *t6 = t5 // t7 = &t3.foo [#0] // t8 = *t7 // t9 = t8() -// t10 = &t3.do [#1] *Doer -// t11 = &t3.foo [#0] *func() -// t12 = *t11 func() -// t13 = changetype Doer <- func() (t12) Doer -// *t10 = t13 +// t10 = &t3.foo [#0] *func() +// t11 = *t10 func() +// t12 = &t3.do [#1] *Doer +// t13 = changetype Doer <- func() (t11) Doer +// *t12 = t13 // t14 = &t3.do [#1] *Doer // t15 = *t14 Doer // t16 = t15() () // Flow chain showing that Baz$1 reaches t8(): -// Baz$1 -> t2 <-> PtrFunction(func()) <-> t5 -> t6 -> t4 <-> Field(testdata.A:foo) <-> t7 -> t8 +// Baz$1 -> t2 <-> PtrFunction(func()) <-> t4 -> t5 -> t6 <-> Field(testdata.A:foo) <-> t7 -> t8 // Flow chain showing that Baz$1 reaches t15(): -// Field(testdata.A:foo) <-> t11 -> t12 -> t13 -> t10 <-> Field(testdata.A:do) <-> t14 -> t15 +// Field(testdata.A:foo) <-> t10 -> t11 -> t13 -> t12 <-> Field(testdata.A:do) <-> t14 -> t15 // WANT: // Local(f) -> Local(t0) // Local(t0) -> PtrFunction(func()) // Function(Baz$1) -> Local(t2) -// PtrFunction(func()) -> Local(t0), Local(t2), Local(t5) +// PtrFunction(func()) -> Local(t0), Local(t2), Local(t4) // Local(t2) -> PtrFunction(func()) -// Local(t4) -> Field(testdata.A:foo) -// Local(t5) -> Local(t6), PtrFunction(func()) -// Local(t6) -> Local(t4) +// Local(t6) -> Field(testdata.A:foo) +// Local(t4) -> Local(t5), PtrFunction(func()) +// Local(t5) -> Local(t6) // Local(t7) -> Field(testdata.A:foo), Local(t8) -// Field(testdata.A:foo) -> Local(t11), Local(t4), Local(t7) -// Local(t4) -> Field(testdata.A:foo) -// Field(testdata.A:do) -> Local(t10), Local(t14) -// Local(t10) -> Field(testdata.A:do) -// Local(t11) -> Field(testdata.A:foo), Local(t12) -// Local(t12) -> Local(t13) -// Local(t13) -> Local(t10) +// Field(testdata.A:foo) -> Local(t10), Local(t6), Local(t7) +// Local(t6) -> Field(testdata.A:foo) +// Field(testdata.A:do) -> Local(t12), Local(t14) +// Local(t12) -> Field(testdata.A:do) +// Local(t10) -> Field(testdata.A:foo), Local(t11) +// Local(t11) -> Local(t13) +// Local(t13) -> Local(t12) // Local(t14) -> Field(testdata.A:do), Local(t15) diff --git a/go/callgraph/vta/testdata/src/panic.go b/go/callgraph/vta/testdata/src/panic.go index 2d39c70ea89..5ef3548577b 100644 --- a/go/callgraph/vta/testdata/src/panic.go +++ b/go/callgraph/vta/testdata/src/panic.go @@ -27,12 +27,12 @@ func recover2() { func Baz(a A) { defer recover1() + defer recover() panic(a) } // Relevant SSA: // func recover1(): -// 0: // t0 = print("only this recover...":string) // t1 = recover() // t2 = typeassert,ok t1.(I) @@ -53,6 +53,7 @@ func Baz(a A) { // t0 = local A (a) // *t0 = a // defer recover1() +// defer recover() // t1 = *t0 // t2 = make interface{} <- A (t1) // panic t2 diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go index 0531a227f6c..c0b5775907f 100644 --- a/go/callgraph/vta/utils.go +++ b/go/callgraph/vta/utils.go @@ -56,24 +56,7 @@ func hasInFlow(n node) bool { return true } - return isInterface(t) || isFunction(t) -} - -// hasInitialTypes check if a node can have initial types. -// Returns true iff `n` is not a panic or recover node as -// those are artificial. -func hasInitialTypes(n node) bool { - switch n.(type) { - case panicArg, recoverReturn: - return false - default: - return true - } -} - -func isInterface(t types.Type) bool { - _, ok := t.Underlying().(*types.Interface) - return ok + return types.IsInterface(t) || isFunction(t) } func isFunction(t types.Type) bool { @@ -98,7 +81,7 @@ func interfaceUnderPtr(t types.Type) types.Type { return nil } - if isInterface(p.Elem()) { + if types.IsInterface(p.Elem()) { return p.Elem() } diff --git a/go/gcexportdata/example_test.go b/go/gcexportdata/example_test.go index e81e705b1c4..cdd68f49c53 100644 --- a/go/gcexportdata/example_test.go +++ b/go/gcexportdata/example_test.go @@ -30,7 +30,6 @@ func ExampleRead() { log.Fatalf("can't find export data for fmt") } fmt.Printf("Package path: %s\n", path) - fmt.Printf("Export data: %s\n", filepath.Base(filename)) // Open and read the file. f, err := os.Open(filename) @@ -80,7 +79,6 @@ func ExampleRead() { // Output: // // Package path: fmt - // Export data: fmt.a // Package members: Println found // Println type: func(a ...any) (n int, err error) // Println location: $GOROOT/src/fmt/print.go:123:1 diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index d50826dbf7e..620446207e2 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -22,26 +22,42 @@ package gcexportdata // import "golang.org/x/tools/go/gcexportdata" import ( "bufio" "bytes" + "encoding/json" "fmt" "go/token" "go/types" "io" "io/ioutil" + "os/exec" - "golang.org/x/tools/go/internal/gcimporter" + "golang.org/x/tools/internal/gcimporter" ) // Find returns the name of an object (.o) or archive (.a) file // containing type information for the specified import path, -// using the workspace layout conventions of go/build. +// using the go command. // If no file was found, an empty filename is returned. // // A relative srcDir is interpreted relative to the current working directory. // // Find also returns the package's resolved (canonical) import path, // reflecting the effects of srcDir and vendoring on importPath. +// +// Deprecated: Use the higher-level API in golang.org/x/tools/go/packages, +// which is more efficient. func Find(importPath, srcDir string) (filename, path string) { - return gcimporter.FindPkg(importPath, srcDir) + cmd := exec.Command("go", "list", "-json", "-export", "--", importPath) + cmd.Dir = srcDir + out, err := cmd.CombinedOutput() + if err != nil { + return "", "" + } + var data struct { + ImportPath string + Export string + } + json.Unmarshal(out, &data) + return data.Export, data.ImportPath } // NewReader returns a reader for the export data section of an object @@ -71,7 +87,11 @@ func NewReader(r io.Reader) (io.Reader, error) { // Read reads export data from in, decodes it, and returns type // information for the package. -// The package name is specified by path. +// +// The package path (effectively its linker symbol prefix) is +// specified by path, since unlike the package name, this information +// may not be recorded in the export data. +// // File position information is added to fset. // // Read may inspect and add to the imports map to ensure that references @@ -100,13 +120,29 @@ func Read(in io.Reader, fset *token.FileSet, imports map[string]*types.Package, // The indexed export format starts with an 'i'; the older // binary export format starts with a 'c', 'd', or 'v' // (from "version"). Select appropriate importer. - if len(data) > 0 && data[0] == 'i' { - _, pkg, err := gcimporter.IImportData(fset, imports, data[1:], path) - return pkg, err - } + if len(data) > 0 { + switch data[0] { + case 'i': + _, pkg, err := gcimporter.IImportData(fset, imports, data[1:], path) + return pkg, err + + case 'v', 'c', 'd': + _, pkg, err := gcimporter.BImportData(fset, imports, data, path) + return pkg, err - _, pkg, err := gcimporter.BImportData(fset, imports, data, path) - return pkg, err + case 'u': + _, pkg, err := gcimporter.UImportData(fset, imports, data[1:], path) + 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]), path) + } + } + return nil, fmt.Errorf("empty export data for %s", path) } // Write writes encoded type information for the specified package to out. diff --git a/go/gcexportdata/importer.go b/go/gcexportdata/importer.go index fe6ed93215c..37a7247e268 100644 --- a/go/gcexportdata/importer.go +++ b/go/gcexportdata/importer.go @@ -22,6 +22,9 @@ import ( // version-skew problems described in the documentation of this package, // or to control the FileSet or access the imports map populated during // package loading. +// +// Deprecated: Use the higher-level API in golang.org/x/tools/go/packages, +// which is more efficient. func NewImporter(fset *token.FileSet, imports map[string]*types.Package) types.ImporterFrom { return importer{fset, imports} } diff --git a/go/packages/golist.go b/go/packages/golist.go index 50533995a65..d9a7915bab0 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -60,6 +60,7 @@ func (r *responseDeduper) addAll(dr *driverResponse) { for _, root := range dr.Roots { r.addRoot(root) } + r.dr.GoVersion = dr.GoVersion } func (r *responseDeduper) addPackage(p *Package) { @@ -302,11 +303,12 @@ func (state *golistState) runContainsQueries(response *responseDeduper, queries } dirResponse, err := state.createDriverResponse(pattern) - // If there was an error loading the package, or the package is returned - // with errors, try to load the file as an ad-hoc package. + // If there was an error loading the package, or no packages are returned, + // or the package is returned with errors, try to load the file as an + // ad-hoc package. // Usually the error will appear in a returned package, but may not if we're // in module mode and the ad-hoc is located outside a module. - if err != nil || len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].GoFiles) == 0 && + if err != nil || len(dirResponse.Packages) == 0 || len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].GoFiles) == 0 && len(dirResponse.Packages[0].Errors) == 1 { var queryErr error if dirResponse, queryErr = state.adhocPackage(pattern, query); queryErr != nil { @@ -453,11 +455,14 @@ func (state *golistState) createDriverResponse(words ...string) (*driverResponse if err != nil { return nil, err } + seen := make(map[string]*jsonPackage) pkgs := make(map[string]*Package) additionalErrors := make(map[string][]Error) // Decode the JSON and convert it to Package form. - var response driverResponse + response := &driverResponse{ + GoVersion: goVersion, + } for dec := json.NewDecoder(buf); dec.More(); { p := new(jsonPackage) if err := dec.Decode(p); err != nil { @@ -729,7 +734,7 @@ func (state *golistState) createDriverResponse(words ...string) (*driverResponse } sort.Slice(response.Packages, func(i, j int) bool { return response.Packages[i].ID < response.Packages[j].ID }) - return &response, nil + return response, nil } func (state *golistState) shouldAddFilenameFromError(p *jsonPackage) bool { @@ -755,6 +760,7 @@ func (state *golistState) shouldAddFilenameFromError(p *jsonPackage) bool { return len(p.Error.ImportStack) == 0 || p.Error.ImportStack[len(p.Error.ImportStack)-1] == p.ImportPath } +// getGoVersion returns the effective minor version of the go command. func (state *golistState) getGoVersion() (int, error) { state.goVersionOnce.Do(func() { state.goVersion, state.goVersionError = gocommand.GoVersion(state.ctx, state.cfgInvocation(), state.cfg.gocmdRunner) diff --git a/go/packages/packages.go b/go/packages/packages.go index a93dc6add4d..54d880d206e 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -19,6 +19,7 @@ import ( "log" "os" "path/filepath" + "runtime" "strings" "sync" "time" @@ -233,6 +234,11 @@ type driverResponse struct { // Imports will be connected and then type and syntax information added in a // later pass (see refine). Packages []*Package + + // GoVersion is the minor version number used by the driver + // (e.g. the go command on the PATH) when selecting .go files. + // Zero means unknown. + GoVersion int } // Load loads and returns the Go packages named by the given patterns. @@ -256,7 +262,7 @@ func Load(cfg *Config, patterns ...string) ([]*Package, error) { return nil, err } l.sizes = response.Sizes - return l.refine(response.Roots, response.Packages...) + return l.refine(response) } // defaultDriver is a driver that implements go/packages' fallback behavior. @@ -532,6 +538,7 @@ type loaderPackage struct { needsrc bool // load from source (Mode >= LoadTypes) needtypes bool // type information is either requested or depended on initial bool // package was matched by a pattern + goVersion int // minor version number of go command on PATH } // loader holds the working state of a single call to load. @@ -618,7 +625,8 @@ func newLoader(cfg *Config) *loader { // refine connects the supplied packages into a graph and then adds type and // and syntax information as requested by the LoadMode. -func (ld *loader) refine(roots []string, list ...*Package) ([]*Package, error) { +func (ld *loader) refine(response *driverResponse) ([]*Package, error) { + roots := response.Roots rootMap := make(map[string]int, len(roots)) for i, root := range roots { rootMap[root] = i @@ -626,7 +634,7 @@ func (ld *loader) refine(roots []string, list ...*Package) ([]*Package, error) { ld.pkgs = make(map[string]*loaderPackage) // first pass, fixup and build the map and roots var initial = make([]*loaderPackage, len(roots)) - for _, pkg := range list { + for _, pkg := range response.Packages { rootIndex := -1 if i, found := rootMap[pkg.ID]; found { rootIndex = i @@ -648,6 +656,7 @@ func (ld *loader) refine(roots []string, list ...*Package) ([]*Package, error) { Package: pkg, needtypes: needtypes, needsrc: needsrc, + goVersion: response.GoVersion, } ld.pkgs[lpkg.ID] = lpkg if rootIndex >= 0 { @@ -923,6 +932,33 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { lpkg.Errors = append(lpkg.Errors, errs...) } + // If the go command on the PATH is newer than the runtime, + // then the go/{scanner,ast,parser,types} packages from the + // standard library may be unable to process the files + // selected by go list. + // + // There is currently no way to downgrade the effective + // version of the go command (see issue 52078), so we proceed + // with the newer go command but, in case of parse or type + // errors, we emit an additional diagnostic. + // + // See: + // - golang.org/issue/52078 (flag to set release tags) + // - golang.org/issue/50825 (gopls legacy version support) + // - golang.org/issue/55883 (go/packages confusing error) + var runtimeVersion int + if _, err := fmt.Sscanf(runtime.Version(), "go1.%d", &runtimeVersion); err == nil && runtimeVersion < lpkg.goVersion { + defer func() { + if len(lpkg.Errors) > 0 { + appendError(Error{ + Pos: "-", + Msg: fmt.Sprintf("This application uses version go1.%d of the source-processing packages but runs version go1.%d of 'go list'. It may fail to process source files that rely on newer language features. If so, rebuild the application using a newer version of Go.", runtimeVersion, lpkg.goVersion), + Kind: UnknownError, + }) + } + }() + } + if ld.Config.Mode&NeedTypes != 0 && len(lpkg.CompiledGoFiles) == 0 && lpkg.ExportFile != "" { // The config requested loading sources and types, but sources are missing. // Add an error to the package and fall back to loading from export data. diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 796edb6b7b4..647f3a366df 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2709,6 +2709,31 @@ func TestEmptyEnvironment(t *testing.T) { } } +func TestPackageLoadSingleFile(t *testing.T) { + tmp, err := ioutil.TempDir("", "a") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + filename := filepath.Join(tmp, "a.go") + + if err := ioutil.WriteFile(filename, []byte(`package main; func main() { println("hello world") }`), 0775); err != nil { + t.Fatal(err) + } + + pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadSyntax, Dir: tmp}, "file="+filename) + if err != nil { + t.Fatalf("could not load package: %v", err) + } + if len(pkgs) != 1 { + t.Fatalf("expected one package to be loaded, got %d", len(pkgs)) + } + if len(pkgs[0].CompiledGoFiles) != 1 || pkgs[0].CompiledGoFiles[0] != filename { + t.Fatalf("expected one compiled go file (%q), got %v", filename, pkgs[0].CompiledGoFiles) + } +} + func errorMessages(errors []packages.Error) []string { var msgs []string for _, err := range errors { diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go index 430258681f5..92c20a64a8d 100644 --- a/go/packages/packagestest/expect.go +++ b/go/packages/packagestest/expect.go @@ -16,7 +16,6 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/span" ) const ( @@ -124,14 +123,31 @@ func (e *Exported) Expect(methods map[string]interface{}) error { return nil } -// Range is a type alias for span.Range for backwards compatibility, prefer -// using span.Range directly. -type Range = span.Range +// A Range represents an interval within a source file in go/token notation. +type Range struct { + TokFile *token.File // non-nil + Start, End token.Pos // both valid and within range of TokFile +} + +// A rangeSetter abstracts a variable that can be set from a Range value. +// +// The parameter conversion machinery will automatically construct a +// variable of type T and call the SetRange method on its address if +// *T implements rangeSetter. This allows alternative notations of +// source ranges to interoperate transparently with this package. +// +// This type intentionally does not mention Range itself, to avoid a +// dependency from the application's range type upon this package. +// +// Currently this is a secret back door for use only by gopls. +type rangeSetter interface { + SetRange(file *token.File, start, end token.Pos) +} // Mark adds a new marker to the known set. func (e *Exported) Mark(name string, r Range) { if e.markers == nil { - e.markers = make(map[string]span.Range) + e.markers = make(map[string]Range) } e.markers[name] = r } @@ -221,22 +237,22 @@ func (e *Exported) getMarkers() error { return nil } // set markers early so that we don't call getMarkers again from Expect - e.markers = make(map[string]span.Range) + e.markers = make(map[string]Range) return e.Expect(map[string]interface{}{ markMethod: e.Mark, }) } var ( - noteType = reflect.TypeOf((*expect.Note)(nil)) - identifierType = reflect.TypeOf(expect.Identifier("")) - posType = reflect.TypeOf(token.Pos(0)) - positionType = reflect.TypeOf(token.Position{}) - rangeType = reflect.TypeOf(span.Range{}) - spanType = reflect.TypeOf(span.Span{}) - fsetType = reflect.TypeOf((*token.FileSet)(nil)) - regexType = reflect.TypeOf((*regexp.Regexp)(nil)) - exportedType = reflect.TypeOf((*Exported)(nil)) + noteType = reflect.TypeOf((*expect.Note)(nil)) + identifierType = reflect.TypeOf(expect.Identifier("")) + posType = reflect.TypeOf(token.Pos(0)) + positionType = reflect.TypeOf(token.Position{}) + rangeType = reflect.TypeOf(Range{}) + rangeSetterType = reflect.TypeOf((*rangeSetter)(nil)).Elem() + fsetType = reflect.TypeOf((*token.FileSet)(nil)) + regexType = reflect.TypeOf((*regexp.Regexp)(nil)) + exportedType = reflect.TypeOf((*Exported)(nil)) ) // converter converts from a marker's argument parsed from the comment to @@ -295,17 +311,16 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } return reflect.ValueOf(r), remains, nil }, nil - case pt == spanType: + case reflect.PtrTo(pt).AssignableTo(rangeSetterType): + // (*pt).SetRange method exists: call it. return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err } - spn, err := r.Span() - if err != nil { - return reflect.Value{}, nil, err - } - return reflect.ValueOf(spn), remains, nil + v := reflect.New(pt) + v.Interface().(rangeSetter).SetRange(r.TokFile, r.Start, r.End) + return v.Elem(), remains, nil }, nil case pt == identifierType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { @@ -408,9 +423,10 @@ func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { } } -func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (span.Range, []interface{}, error) { +func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (Range, []interface{}, error) { + tokFile := e.ExpectFileSet.File(n.Pos) if len(args) < 1 { - return span.Range{}, nil, fmt.Errorf("missing argument") + return Range{}, nil, fmt.Errorf("missing argument") } arg := args[0] args = args[1:] @@ -419,37 +435,62 @@ func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (span.Rang // handle the special identifiers switch arg { case eofIdentifier: - // end of file identifier, look up the current file - f := e.ExpectFileSet.File(n.Pos) - eof := f.Pos(f.Size()) - return span.NewRange(e.ExpectFileSet, eof, token.NoPos), args, nil + // end of file identifier + eof := tokFile.Pos(tokFile.Size()) + return newRange(tokFile, eof, eof), args, nil default: // look up an marker by name mark, ok := e.markers[string(arg)] if !ok { - return span.Range{}, nil, fmt.Errorf("cannot find marker %v", arg) + return Range{}, nil, fmt.Errorf("cannot find marker %v", arg) } return mark, args, nil } case string: start, end, err := expect.MatchBefore(e.ExpectFileSet, e.FileContents, n.Pos, arg) if err != nil { - return span.Range{}, nil, err + return Range{}, nil, err } - if start == token.NoPos { - return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) + if !start.IsValid() { + return Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) } - return span.NewRange(e.ExpectFileSet, start, end), args, nil + return newRange(tokFile, start, end), args, nil case *regexp.Regexp: start, end, err := expect.MatchBefore(e.ExpectFileSet, e.FileContents, n.Pos, arg) if err != nil { - return span.Range{}, nil, err + return Range{}, nil, err } - if start == token.NoPos { - return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) + if !start.IsValid() { + return Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) } - return span.NewRange(e.ExpectFileSet, start, end), args, nil + return newRange(tokFile, start, end), args, nil default: - return span.Range{}, nil, fmt.Errorf("cannot convert %v to pos", arg) + return Range{}, nil, fmt.Errorf("cannot convert %v to pos", arg) + } +} + +// newRange creates a new Range from a token.File and two valid positions within it. +func newRange(file *token.File, start, end token.Pos) Range { + fileBase := file.Base() + fileEnd := fileBase + file.Size() + if !start.IsValid() { + panic("invalid start token.Pos") + } + if !end.IsValid() { + panic("invalid end token.Pos") + } + if int(start) < fileBase || int(start) > fileEnd { + panic(fmt.Sprintf("invalid start: %d not in [%d, %d]", start, fileBase, fileEnd)) + } + if int(end) < fileBase || int(end) > fileEnd { + panic(fmt.Sprintf("invalid end: %d not in [%d, %d]", end, fileBase, fileEnd)) + } + if start > end { + panic("invalid start: greater than end") + } + return Range{ + TokFile: file, + Start: start, + End: end, } } diff --git a/go/packages/packagestest/expect_test.go b/go/packages/packagestest/expect_test.go index 2587f580b06..46d96d61fb9 100644 --- a/go/packages/packagestest/expect_test.go +++ b/go/packages/packagestest/expect_test.go @@ -10,7 +10,6 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages/packagestest" - "golang.org/x/tools/internal/span" ) func TestExpect(t *testing.T) { @@ -43,7 +42,7 @@ func TestExpect(t *testing.T) { } }, "directNote": func(n *expect.Note) {}, - "range": func(r span.Range) { + "range": func(r packagestest.Range) { if r.Start == token.NoPos || r.Start == 0 { t.Errorf("Range had no valid starting position") } diff --git a/go/packages/packagestest/export.go b/go/packages/packagestest/export.go index 894dcdd445d..b687a44fb4f 100644 --- a/go/packages/packagestest/export.go +++ b/go/packages/packagestest/export.go @@ -79,7 +79,6 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -129,7 +128,7 @@ type Exported struct { primary string // the first non GOROOT module that was exported written map[string]map[string]string // the full set of exported files notes []*expect.Note // The list of expectations extracted from go source files - markers map[string]span.Range // The set of markers extracted from go source files + markers map[string]Range // The set of markers extracted from go source files } // Exporter implementations are responsible for converting from the generic description of some diff --git a/go/ssa/builder.go b/go/ssa/builder.go index b36775a4e34..8ec8f6e310b 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -453,12 +453,16 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { } wantAddr := true v := b.receiver(fn, e.X, wantAddr, escaping, sel) - last := len(sel.index) - 1 - return &address{ - addr: emitFieldSelection(fn, v, sel.index[last], true, e.Sel), - pos: e.Sel.Pos(), - expr: e.Sel, + index := sel.index[len(sel.index)-1] + fld := typeparams.CoreType(deref(v.Type())).(*types.Struct).Field(index) + + // Due to the two phases of resolving AssignStmt, a panic from x.f = p() + // when x is nil is required to come after the side-effects of + // evaluating x and p(). + emit := func(fn *Function) Value { + return emitFieldSelection(fn, v, index, true, e.Sel) } + return &lazyAddress{addr: emit, t: fld.Type(), pos: e.Sel.Pos(), expr: e.Sel} case *ast.IndexExpr: var x Value @@ -487,13 +491,19 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { if isUntyped(index.Type()) { index = emitConv(fn, index, tInt) } - v := &IndexAddr{ - X: x, - Index: index, + // Due to the two phases of resolving AssignStmt, a panic from x[i] = p() + // when x is nil or i is out-of-bounds is required to come after the + // side-effects of evaluating x, i and p(). + emit := func(fn *Function) Value { + v := &IndexAddr{ + X: x, + Index: index, + } + v.setPos(e.Lbrack) + v.setType(et) + return fn.emit(v) } - v.setPos(e.Lbrack) - v.setType(et) - return &address{addr: fn.emit(v), pos: e.Lbrack, expr: e} + return &lazyAddress{addr: emit, t: deref(et), pos: e.Lbrack, expr: e} case *ast.StarExpr: return &address{addr: b.expr(fn, e.X), pos: e.Star, expr: e} @@ -669,6 +679,8 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { y.pos = e.Lparen case *SliceToArrayPointer: y.pos = e.Lparen + case *UnOp: // conversion from slice to array. + y.pos = e.Lparen } } return y @@ -2461,6 +2473,9 @@ func (p *Package) build() { } // Initialize package-level vars in correct order. + if len(p.info.InitOrder) > 0 && len(p.files) == 0 { + panic("no source files provided for package. cannot initialize globals") + } for _, varinit := range p.info.InitOrder { if init.Prog.mode&LogSource != 0 { fmt.Fprintf(os.Stderr, "build global initializer %v @ %s\n", diff --git a/go/ssa/builder_go117_test.go b/go/ssa/builder_go117_test.go index f6545e5e2cf..69985970596 100644 --- a/go/ssa/builder_go117_test.go +++ b/go/ssa/builder_go117_test.go @@ -57,7 +57,6 @@ func TestBuildPackageFailuresGo117(t *testing.T) { importer types.Importer }{ {"slice to array pointer - source is not a slice", "package p; var s [4]byte; var _ = (*[4]byte)(s)", nil}, - {"slice to array pointer - dest is not a pointer", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, {"slice to array pointer - dest pointer elem is not an array", "package p; var s []byte; var _ = (*byte)(s)", nil}, } diff --git a/go/ssa/builder_go120_test.go b/go/ssa/builder_go120_test.go new file mode 100644 index 00000000000..84bdd4c41ab --- /dev/null +++ b/go/ssa/builder_go120_test.go @@ -0,0 +1,48 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.20 +// +build go1.20 + +package ssa_test + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func TestBuildPackageGo120(t *testing.T) { + tests := []struct { + name string + src string + importer types.Importer + }{ + {"slice to array", "package p; var s []byte; var _ = ([4]byte)(s)", nil}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", tc.src, parser.ParseComments) + if err != nil { + t.Error(err) + } + files := []*ast.File{f} + + pkg := types.NewPackage("p", "") + conf := &types.Config{Importer: tc.importer} + if _, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions); err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + } +} diff --git a/go/ssa/dom.go b/go/ssa/dom.go index ce2473cafce..66a2f5e6ed3 100644 --- a/go/ssa/dom.go +++ b/go/ssa/dom.go @@ -303,7 +303,7 @@ func sanityCheckDomTree(f *Function) { // Printing functions ---------------------------------------- -// printDomTree prints the dominator tree as text, using indentation. +// printDomTreeText prints the dominator tree as text, using indentation. func printDomTreeText(buf *bytes.Buffer, v *BasicBlock, indent int) { fmt.Fprintf(buf, "%*s%s\n", 4*indent, "", v) for _, child := range v.dom.children { diff --git a/go/ssa/emit.go b/go/ssa/emit.go index fb11c3558d3..f6537acc97f 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -177,7 +177,6 @@ func emitConv(f *Function, val Value, typ types.Type) Value { if types.Identical(t_src, typ) { return val } - ut_dst := typ.Underlying() ut_src := t_src.Underlying() @@ -229,12 +228,32 @@ func emitConv(f *Function, val Value, typ types.Type) Value { // Conversion from slice to array pointer? if slice, ok := ut_src.(*types.Slice); ok { - if ptr, ok := ut_dst.(*types.Pointer); ok { + switch t := ut_dst.(type) { + case *types.Pointer: + ptr := t if arr, ok := ptr.Elem().Underlying().(*types.Array); ok && types.Identical(slice.Elem(), arr.Elem()) { c := &SliceToArrayPointer{X: val} - c.setType(ut_dst) + // TODO(taking): Check if this should be ut_dst or ptr. + c.setType(ptr) return f.emit(c) } + case *types.Array: + arr := t + if arr.Len() == 0 { + return zeroValue(f, arr) + } + if types.Identical(slice.Elem(), arr.Elem()) { + c := &SliceToArrayPointer{X: val} + c.setType(types.NewPointer(arr)) + x := f.emit(c) + unOp := &UnOp{ + Op: token.MUL, + X: x, + CommaOk: false, + } + unOp.setType(typ) + return f.emit(unOp) + } } } // A representation-changing conversion? diff --git a/go/ssa/interp/interp_go120_test.go b/go/ssa/interp/interp_go120_test.go new file mode 100644 index 00000000000..d8eb2c21341 --- /dev/null +++ b/go/ssa/interp/interp_go120_test.go @@ -0,0 +1,12 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.20 +// +build go1.20 + +package interp_test + +func init() { + testdataTests = append(testdataTests, "slice2array.go") +} diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index a0acf2f968a..51a74015c95 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go @@ -127,6 +127,7 @@ var testdataTests = []string{ "width32.go", "fixedbugs/issue52342.go", + "fixedbugs/issue55086.go", } func init() { diff --git a/go/ssa/interp/testdata/fixedbugs/issue55086.go b/go/ssa/interp/testdata/fixedbugs/issue55086.go new file mode 100644 index 00000000000..84c81e91a26 --- /dev/null +++ b/go/ssa/interp/testdata/fixedbugs/issue55086.go @@ -0,0 +1,132 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +func a() (r string) { + s := "initial" + var p *struct{ i int } + defer func() { + recover() + r = s + }() + + s, p.i = "set", 2 // s must be set before p.i panics + return "unreachable" +} + +func b() (r string) { + s := "initial" + fn := func() []int { panic("") } + defer func() { + recover() + r = s + }() + + s, fn()[0] = "set", 2 // fn() panics before any assignment occurs + return "unreachable" +} + +func c() (r string) { + s := "initial" + var p map[int]int + defer func() { + recover() + r = s + }() + + s, p[0] = "set", 2 //s must be set before p[0] index panics" + return "unreachable" +} + +func d() (r string) { + s := "initial" + var p map[int]int + defer func() { + recover() + r = s + }() + fn := func() int { panic("") } + + s, p[0] = "set", fn() // fn() panics before s is set + return "unreachable" +} + +func e() (r string) { + s := "initial" + p := map[int]int{} + defer func() { + recover() + r = s + }() + fn := func() int { panic("") } + + s, p[fn()] = "set", 0 // fn() panics before any assignment occurs + return "unreachable" +} + +func f() (r string) { + s := "initial" + p := []int{} + defer func() { + recover() + r = s + }() + + s, p[1] = "set", 0 // p[1] panics after s is set + return "unreachable" +} + +func g() (r string) { + s := "initial" + p := map[any]any{} + defer func() { + recover() + r = s + }() + var i any = func() {} + s, p[i] = "set", 0 // p[i] panics after s is set + return "unreachable" +} + +func h() (r string) { + fail := false + defer func() { + recover() + if fail { + r = "fail" + } else { + r = "success" + } + }() + + type T struct{ f int } + var p *struct{ *T } + + // The implicit "p.T" operand should be evaluated in phase 1 (and panic), + // before the "fail = true" assignment in phase 2. + fail, p.f = true, 0 + return "unreachable" +} + +func main() { + for _, test := range []struct { + fn func() string + want string + desc string + }{ + {a, "set", "s must be set before p.i panics"}, + {b, "initial", "p() panics before s is set"}, + {c, "set", "s must be set before p[0] index panics"}, + {d, "initial", "fn() panics before s is set"}, + {e, "initial", "fn() panics before s is set"}, + {f, "set", "p[1] panics after s is set"}, + {g, "set", "p[i] panics after s is set"}, + {h, "success", "p.T panics before fail is set"}, + } { + if test.fn() != test.want { + panic(test.desc) + } + } +} diff --git a/go/ssa/interp/testdata/slice2array.go b/go/ssa/interp/testdata/slice2array.go new file mode 100644 index 00000000000..43c0543eabf --- /dev/null +++ b/go/ssa/interp/testdata/slice2array.go @@ -0,0 +1,56 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Test for slice to array conversion introduced in go1.20 +// See: https://tip.golang.org/ref/spec#Conversions_from_slice_to_array_pointer + +package main + +func main() { + s := make([]byte, 3, 4) + s[0], s[1], s[2] = 2, 3, 5 + a := ([2]byte)(s) + s[0] = 7 + + if a != [2]byte{2, 3} { + panic("converted from non-nil slice to array") + } + + { + var s []int + a:= ([0]int)(s) + if a != [0]int{} { + panic("zero len array is not equal") + } + } + + if emptyToEmptyDoesNotPanic() { + panic("no panic expected from emptyToEmptyDoesNotPanic()") + } + if !threeToFourDoesPanic() { + panic("panic expected from threeToFourDoesPanic()") + } +} + +func emptyToEmptyDoesNotPanic() (raised bool) { + defer func() { + if e := recover(); e != nil { + raised = true + } + }() + var s []int + _ = ([0]int)(s) + return false +} + +func threeToFourDoesPanic() (raised bool) { + defer func() { + if e := recover(); e != nil { + raised = true + } + }() + s := make([]int, 3, 5) + _ = ([4]int)(s) + return false +} \ No newline at end of file diff --git a/go/ssa/lvalue.go b/go/ssa/lvalue.go index 64262def8b2..455b1e50fa4 100644 --- a/go/ssa/lvalue.go +++ b/go/ssa/lvalue.go @@ -93,6 +93,42 @@ func (e *element) typ() types.Type { return e.t } +// A lazyAddress is an lvalue whose address is the result of an instruction. +// These work like an *address except a new address.address() Value +// is created on each load, store and address call. +// A lazyAddress can be used to control when a side effect (nil pointer +// dereference, index out of bounds) of using a location happens. +type lazyAddress struct { + addr func(fn *Function) Value // emit to fn the computation of the address + t types.Type // type of the location + pos token.Pos // source position + expr ast.Expr // source syntax of the value (not address) [debug mode] +} + +func (l *lazyAddress) load(fn *Function) Value { + load := emitLoad(fn, l.addr(fn)) + load.pos = l.pos + return load +} + +func (l *lazyAddress) store(fn *Function, v Value) { + store := emitStore(fn, l.addr(fn), v, l.pos) + if l.expr != nil { + // store.Val is v, converted for assignability. + emitDebugRef(fn, l.expr, store.Val, false) + } +} + +func (l *lazyAddress) address(fn *Function) Value { + addr := l.addr(fn) + if l.expr != nil { + emitDebugRef(fn, l.expr, addr, true) + } + return addr +} + +func (l *lazyAddress) typ() types.Type { return l.t } + // A blank is a dummy variable whose name is "_". // It is not reified: loads are illegal and stores are ignored. type blank struct{} diff --git a/go/ssa/ssautil/load.go b/go/ssa/ssautil/load.go index 58d185f6727..96d69a20a17 100644 --- a/go/ssa/ssautil/load.go +++ b/go/ssa/ssautil/load.go @@ -77,10 +77,12 @@ func doPackages(initial []*packages.Package, mode ssa.BuilderMode, deps bool) (* packages.Visit(initial, nil, func(p *packages.Package) { if p.Types != nil && !p.IllTyped { var files []*ast.File + var info *types.Info if deps || isInitial[p] { files = p.Syntax + info = p.TypesInfo } - ssamap[p] = prog.CreatePackage(p.Types, files, p.TypesInfo, true) + ssamap[p] = prog.CreatePackage(p.Types, files, info, true) } }) diff --git a/go/ssa/ssautil/load_test.go b/go/ssa/ssautil/load_test.go index f769be273bb..707a1d0b69d 100644 --- a/go/ssa/ssautil/load_test.go +++ b/go/ssa/ssautil/load_test.go @@ -12,10 +12,12 @@ import ( "go/token" "go/types" "os" + "path" "strings" "testing" "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" "golang.org/x/tools/internal/testenv" @@ -135,3 +137,57 @@ func TestIssue28106(t *testing.T) { prog, _ := ssautil.Packages(pkgs, ssa.BuilderMode(0)) prog.Build() // no crash } + +func TestIssue53604(t *testing.T) { + // Tests that variable initializers are not added to init() when syntax + // is not present but types.Info is available. + // + // Packages x, y, z are loaded with mode `packages.LoadSyntax`. + // Package x imports y, and y imports z. + // Packages are built using ssautil.Packages() with x and z as roots. + // This setup creates y using CreatePackage(pkg, files, info, ...) + // where len(files) == 0 but info != nil. + // + // Tests that globals from y are not initialized. + e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{ + { + Name: "golang.org/fake", + Files: map[string]interface{}{ + "x/x.go": `package x; import "golang.org/fake/y"; var V = y.F()`, + "y/y.go": `package y; import "golang.org/fake/z"; var F = func () *int { return &z.Z } `, + "z/z.go": `package z; var Z int`, + }, + }, + }) + defer e.Cleanup() + + // Load x and z as entry packages using packages.LoadSyntax + e.Config.Mode = packages.LoadSyntax + pkgs, err := packages.Load(e.Config, path.Join(e.Temp(), "fake/x"), path.Join(e.Temp(), "fake/z")) + if err != nil { + t.Fatal(err) + } + for _, p := range pkgs { + if len(p.Errors) > 0 { + t.Fatalf("%v", p.Errors) + } + } + + prog, _ := ssautil.Packages(pkgs, ssa.BuilderMode(0)) + prog.Build() + + // y does not initialize F. + y := prog.ImportedPackage("golang.org/fake/y") + if y == nil { + t.Fatal("Failed to load intermediate package y") + } + yinit := y.Members["init"].(*ssa.Function) + for _, bb := range yinit.Blocks { + for _, i := range bb.Instrs { + if store, ok := i.(*ssa.Store); ok && store.Addr == y.Var("F") { + t.Errorf("y.init() stores to F %v", store) + } + } + } + +} diff --git a/go/types/typeutil/map.go b/go/types/typeutil/map.go index dcc029b8733..7bd2fdb38be 100644 --- a/go/types/typeutil/map.go +++ b/go/types/typeutil/map.go @@ -332,7 +332,9 @@ func (h Hasher) hashFor(t types.Type) uint32 { // Method order is not significant. // Ignore m.Pkg(). m := t.Method(i) - hash += 3*hashString(m.Name()) + 5*h.Hash(m.Type()) + // Use shallow hash on method signature to + // avoid anonymous interface cycles. + hash += 3*hashString(m.Name()) + 5*h.shallowHash(m.Type()) } // Hash type restrictions. @@ -434,3 +436,76 @@ func (h Hasher) hashPtr(ptr interface{}) uint32 { h.ptrMap[ptr] = hash return hash } + +// shallowHash computes a hash of t without looking at any of its +// element Types, to avoid potential anonymous cycles in the types of +// interface methods. +// +// When an unnamed non-empty interface type appears anywhere among the +// arguments or results of an interface method, there is a potential +// for endless recursion. Consider: +// +// type X interface { m() []*interface { X } } +// +// The problem is that the Methods of the interface in m's result type +// 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 { + // t is the type of an interface method (Signature), + // its params or results (Tuples), or their immediate + // elements (mostly Slice, Pointer, Basic, Named), + // so there's no need to optimize anything else. + switch t := t.(type) { + case *types.Signature: + var hash uint32 = 604171 + if t.Variadic() { + hash *= 971767 + } + // The Signature/Tuple recursion is always finite + // and invariably shallow. + return hash + 1062599*h.shallowHash(t.Params()) + 1282529*h.shallowHash(t.Results()) + + case *types.Tuple: + n := t.Len() + hash := 9137 + 2*uint32(n) + for i := 0; i < n; i++ { + hash += 53471161 * h.shallowHash(t.At(i).Type()) + } + return hash + + case *types.Basic: + return 45212177 * uint32(t.Kind()) + + case *types.Array: + return 1524181 + 2*uint32(t.Len()) + + case *types.Slice: + return 2690201 + + case *types.Struct: + return 3326489 + + case *types.Pointer: + return 4393139 + + case *typeparams.Union: + return 562448657 + + case *types.Interface: + return 2124679 // no recursion here + + case *types.Map: + return 9109 + + case *types.Chan: + return 9127 + + case *types.Named: + return h.hashPtr(t.Obj()) + + case *typeparams.TypeParam: + return h.hashPtr(t.Obj()) + } + 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 8cd643e5b48..ee73ff9cfd5 100644 --- a/go/types/typeutil/map_test.go +++ b/go/types/typeutil/map_test.go @@ -244,6 +244,14 @@ func Bar[P Constraint[P]]() {} func Baz[Q any]() {} // The underlying type of Constraint[P] is any. // But Quux is not. func Quux[Q interface{ quux() }]() {} + + +type Issue56048_I interface{ m() interface { Issue56048_I } } +var Issue56048 = Issue56048_I.m + +type Issue56048_Ib interface{ m() chan []*interface { Issue56048_Ib } } +var Issue56048b = Issue56048_Ib.m + ` fset := token.NewFileSet() @@ -296,12 +304,14 @@ func Quux[Q interface{ quux() }]() {} ME1Type = scope.Lookup("ME1Type").Type() ME2 = scope.Lookup("ME2").Type() - Constraint = scope.Lookup("Constraint").Type() - Foo = scope.Lookup("Foo").Type() - Fn = scope.Lookup("Fn").Type() - Bar = scope.Lookup("Foo").Type() - Baz = scope.Lookup("Foo").Type() - Quux = scope.Lookup("Quux").Type() + Constraint = scope.Lookup("Constraint").Type() + Foo = scope.Lookup("Foo").Type() + Fn = scope.Lookup("Fn").Type() + Bar = scope.Lookup("Foo").Type() + Baz = scope.Lookup("Foo").Type() + Quux = scope.Lookup("Quux").Type() + Issue56048 = scope.Lookup("Issue56048").Type() + Issue56048b = scope.Lookup("Issue56048b").Type() ) tmap := new(typeutil.Map) @@ -371,6 +381,9 @@ func Quux[Q interface{ quux() }]() {} {Bar, "Bar", false}, {Baz, "Baz", false}, {Quux, "Quux", true}, + + {Issue56048, "Issue56048", true}, // (not actually about generics) + {Issue56048b, "Issue56048b", true}, // (not actually about generics) } for _, step := range steps { diff --git a/godoc/godoc.go b/godoc/godoc.go index 7ff2eab6239..dfac2111a67 100644 --- a/godoc/godoc.go +++ b/godoc/godoc.go @@ -345,11 +345,16 @@ func isDigit(ch rune) bool { return '0' <= ch && ch <= '9' || ch >= utf8.RuneSelf && unicode.IsDigit(ch) } -func comment_htmlFunc(comment string) string { +func comment_htmlFunc(info *PageInfo, comment string) string { var buf bytes.Buffer // TODO(gri) Provide list of words (e.g. function parameters) // to be emphasized by ToHTML. - doc.ToHTML(&buf, comment, nil) // does html-escaping + + // godocToHTML is: + // - buf.Write(info.PDoc.HTML(comment)) on go1.19 + // - go/doc.ToHTML(&buf, comment, nil) on other versions + godocToHTML(&buf, info.PDoc, comment) + return buf.String() } @@ -448,7 +453,7 @@ func srcToPkgLinkFunc(relpath string) string { return fmt.Sprintf(`%s`, relpath, relpath[len("pkg/"):]) } -// srcBreadcrumbFun converts each segment of relpath to a HTML . +// srcBreadcrumbFunc converts each segment of relpath to a HTML . // Each segment links to its corresponding src directories. func srcBreadcrumbFunc(relpath string) string { segments := strings.Split(relpath, "/") @@ -658,7 +663,7 @@ func (p *Presentation) example_suffixFunc(name string) string { return suffix } -// implements_html returns the "> Implements" toggle for a package-level named type. +// implements_htmlFunc returns the "> Implements" toggle for a package-level named type. // Its contents are populated from JSON data by client-side JS at load time. func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) string { if p.ImplementsHTML == nil { @@ -676,7 +681,7 @@ func (p *Presentation) implements_htmlFunc(info *PageInfo, typeName string) stri return buf.String() } -// methodset_html returns the "> Method set" toggle for a package-level named type. +// methodset_htmlFunc returns the "> Method set" toggle for a package-level named type. // Its contents are populated from JSON data by client-side JS at load time. func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) string { if p.MethodSetHTML == nil { @@ -694,7 +699,7 @@ func (p *Presentation) methodset_htmlFunc(info *PageInfo, typeName string) strin return buf.String() } -// callgraph_html returns the "> Call graph" toggle for a package-level func. +// callgraph_htmlFunc returns the "> Call graph" toggle for a package-level func. // Its contents are populated from JSON data by client-side JS at load time. func (p *Presentation) callgraph_htmlFunc(info *PageInfo, recv, name string) string { if p.CallGraphHTML == nil { diff --git a/godoc/index.go b/godoc/index.go index d3f9f64fc5c..4dc3362a7e2 100644 --- a/godoc/index.go +++ b/godoc/index.go @@ -50,6 +50,7 @@ import ( "index/suffixarray" "io" "log" + "math" "os" pathpkg "path" "path/filepath" @@ -161,7 +162,7 @@ func newKindRun(h RunList) interface{} { // bit is always the same for all infos in one // list we can simply compare the entire info. k := 0 - prev := SpotInfo(1<<32 - 1) // an unlikely value + prev := SpotInfo(math.MaxUint32) // an unlikely value for _, x := range run { if x != prev { run[k] = x diff --git a/godoc/redirect/hash.go b/godoc/redirect/hash.go deleted file mode 100644 index d5a1e3eb67b..00000000000 --- a/godoc/redirect/hash.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2014 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This file provides a compact encoding of -// a map of Mercurial hashes to Git hashes. - -package redirect - -import ( - "encoding/binary" - "fmt" - "io" - "os" - "sort" - "strconv" - "strings" -) - -// hashMap is a map of Mercurial hashes to Git hashes. -type hashMap struct { - file *os.File - entries int -} - -// newHashMap takes a file handle that contains a map of Mercurial to Git -// hashes. The file should be a sequence of pairs of little-endian encoded -// uint32s, representing a hgHash and a gitHash respectively. -// The sequence must be sorted by hgHash. -// The file must remain open for as long as the returned hashMap is used. -func newHashMap(f *os.File) (*hashMap, error) { - fi, err := f.Stat() - if err != nil { - return nil, err - } - return &hashMap{file: f, entries: int(fi.Size() / 8)}, nil -} - -// Lookup finds an hgHash in the map that matches the given prefix, and returns -// its corresponding gitHash. The prefix must be at least 8 characters long. -func (m *hashMap) Lookup(s string) gitHash { - if m == nil { - return 0 - } - hg, err := hgHashFromString(s) - if err != nil { - return 0 - } - var git gitHash - b := make([]byte, 8) - sort.Search(m.entries, func(i int) bool { - n, err := m.file.ReadAt(b, int64(i*8)) - if err != nil { - panic(err) - } - if n != 8 { - panic(io.ErrUnexpectedEOF) - } - v := hgHash(binary.LittleEndian.Uint32(b[:4])) - if v == hg { - git = gitHash(binary.LittleEndian.Uint32(b[4:])) - } - return v >= hg - }) - return git -} - -// hgHash represents the lower (leftmost) 32 bits of a Mercurial hash. -type hgHash uint32 - -func (h hgHash) String() string { - return intToHash(int64(h)) -} - -func hgHashFromString(s string) (hgHash, error) { - if len(s) < 8 { - return 0, fmt.Errorf("string too small: len(s) = %d", len(s)) - } - hash := s[:8] - i, err := strconv.ParseInt(hash, 16, 64) - if err != nil { - return 0, err - } - return hgHash(i), nil -} - -// gitHash represents the leftmost 28 bits of a Git hash in its upper 28 bits, -// and it encodes hash's repository in the lower 4 bits. -type gitHash uint32 - -func (h gitHash) Hash() string { - return intToHash(int64(h))[:7] -} - -func (h gitHash) Repo() string { - return repo(h & 0xF).String() -} - -func intToHash(i int64) string { - s := strconv.FormatInt(i, 16) - if len(s) < 8 { - s = strings.Repeat("0", 8-len(s)) + s - } - return s -} - -// repo represents a Go Git repository. -type repo byte - -const ( - repoGo repo = iota - repoBlog - repoCrypto - repoExp - repoImage - repoMobile - repoNet - repoSys - repoTalks - repoText - repoTools -) - -func (r repo) String() string { - return map[repo]string{ - repoGo: "go", - repoBlog: "blog", - repoCrypto: "crypto", - repoExp: "exp", - repoImage: "image", - repoMobile: "mobile", - repoNet: "net", - repoSys: "sys", - repoTalks: "talks", - repoText: "text", - repoTools: "tools", - }[r] -} diff --git a/godoc/redirect/redirect.go b/godoc/redirect/redirect.go index 57d779ccb41..d0145ee183b 100644 --- a/godoc/redirect/redirect.go +++ b/godoc/redirect/redirect.go @@ -3,147 +3,22 @@ // license that can be found in the LICENSE file. // Package redirect provides hooks to register HTTP handlers that redirect old -// godoc paths to their new equivalents and assist in accessing the issue -// tracker, wiki, code review system, etc. +// godoc paths to their new equivalents. package redirect // import "golang.org/x/tools/godoc/redirect" import ( - "context" - "fmt" - "html/template" "net/http" - "os" "regexp" - "strconv" - "strings" - "sync" - "time" - - "golang.org/x/net/context/ctxhttp" ) -// Register registers HTTP handlers that redirect old godoc paths to their new -// equivalents and assist in accessing the issue tracker, wiki, code review -// system, etc. If mux is nil it uses http.DefaultServeMux. +// Register registers HTTP handlers that redirect old godoc paths to their new equivalents. +// If mux is nil it uses http.DefaultServeMux. func Register(mux *http.ServeMux) { if mux == nil { mux = http.DefaultServeMux } - handlePathRedirects(mux, pkgRedirects, "/pkg/") - handlePathRedirects(mux, cmdRedirects, "/cmd/") - for prefix, redirect := range prefixHelpers { - p := "/" + prefix + "/" - mux.Handle(p, PrefixHandler(p, redirect)) - } - for path, redirect := range redirects { - mux.Handle(path, Handler(redirect)) - } // NB: /src/pkg (sans trailing slash) is the index of packages. mux.HandleFunc("/src/pkg/", srcPkgHandler) - mux.HandleFunc("/cl/", clHandler) - mux.HandleFunc("/change/", changeHandler) - mux.HandleFunc("/design/", designHandler) -} - -func handlePathRedirects(mux *http.ServeMux, redirects map[string]string, prefix string) { - for source, target := range redirects { - h := Handler(prefix + target + "/") - p := prefix + source - mux.Handle(p, h) - mux.Handle(p+"/", h) - } -} - -// Packages that were renamed between r60 and go1. -var pkgRedirects = map[string]string{ - "asn1": "encoding/asn1", - "big": "math/big", - "cmath": "math/cmplx", - "csv": "encoding/csv", - "exec": "os/exec", - "exp/template/html": "html/template", - "gob": "encoding/gob", - "http": "net/http", - "http/cgi": "net/http/cgi", - "http/fcgi": "net/http/fcgi", - "http/httptest": "net/http/httptest", - "http/pprof": "net/http/pprof", - "json": "encoding/json", - "mail": "net/mail", - "rand": "math/rand", - "rpc": "net/rpc", - "rpc/jsonrpc": "net/rpc/jsonrpc", - "scanner": "text/scanner", - "smtp": "net/smtp", - "tabwriter": "text/tabwriter", - "template": "text/template", - "template/parse": "text/template/parse", - "url": "net/url", - "utf16": "unicode/utf16", - "utf8": "unicode/utf8", - "xml": "encoding/xml", -} - -// Commands that were renamed between r60 and go1. -var cmdRedirects = map[string]string{ - "gofix": "fix", - "goinstall": "go", - "gopack": "pack", - "gotest": "go", - "govet": "vet", - "goyacc": "yacc", -} - -var redirects = map[string]string{ - "/blog": "/blog/", - "/build": "http://build.golang.org", - "/change": "https://go.googlesource.com/go", - "/cl": "https://go-review.googlesource.com", - "/cmd/godoc/": "https://pkg.go.dev/golang.org/x/tools/cmd/godoc", - "/issue": "https://github.com/golang/go/issues", - "/issue/new": "https://github.com/golang/go/issues/new", - "/issues": "https://github.com/golang/go/issues", - "/issues/new": "https://github.com/golang/go/issues/new", - "/play": "http://play.golang.org", - "/design": "https://go.googlesource.com/proposal/+/master/design", - - // In Go 1.2 the references page is part of /doc/. - "/ref": "/doc/#references", - // This next rule clobbers /ref/spec and /ref/mem. - // TODO(adg): figure out what to do here, if anything. - // "/ref/": "/doc/#references", - - // Be nice to people who are looking in the wrong place. - "/doc/mem": "/ref/mem", - "/doc/spec": "/ref/spec", - - "/talks": "http://talks.golang.org", - "/tour": "http://tour.golang.org", - "/wiki": "https://github.com/golang/go/wiki", - - "/doc/articles/c_go_cgo.html": "/blog/c-go-cgo", - "/doc/articles/concurrency_patterns.html": "/blog/go-concurrency-patterns-timing-out-and", - "/doc/articles/defer_panic_recover.html": "/blog/defer-panic-and-recover", - "/doc/articles/error_handling.html": "/blog/error-handling-and-go", - "/doc/articles/gobs_of_data.html": "/blog/gobs-of-data", - "/doc/articles/godoc_documenting_go_code.html": "/blog/godoc-documenting-go-code", - "/doc/articles/gos_declaration_syntax.html": "/blog/gos-declaration-syntax", - "/doc/articles/image_draw.html": "/blog/go-imagedraw-package", - "/doc/articles/image_package.html": "/blog/go-image-package", - "/doc/articles/json_and_go.html": "/blog/json-and-go", - "/doc/articles/json_rpc_tale_of_interfaces.html": "/blog/json-rpc-tale-of-interfaces", - "/doc/articles/laws_of_reflection.html": "/blog/laws-of-reflection", - "/doc/articles/slices_usage_and_internals.html": "/blog/go-slices-usage-and-internals", - "/doc/go_for_cpp_programmers.html": "/wiki/GoForCPPProgrammers", - "/doc/go_tutorial.html": "http://tour.golang.org/", -} - -var prefixHelpers = map[string]string{ - "issue": "https://github.com/golang/go/issues/", - "issues": "https://github.com/golang/go/issues/", - "play": "http://play.golang.org/", - "talks": "http://talks.golang.org/", - "wiki": "https://github.com/golang/go/wiki/", } func Handler(target string) http.Handler { @@ -181,144 +56,3 @@ func srcPkgHandler(w http.ResponseWriter, r *http.Request) { r.URL.Path = "/src/" + r.URL.Path[len("/src/pkg/"):] http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) } - -func clHandler(w http.ResponseWriter, r *http.Request) { - const prefix = "/cl/" - if p := r.URL.Path; p == prefix { - // redirect /prefix/ to /prefix - http.Redirect(w, r, p[:len(p)-1], http.StatusFound) - return - } - id := r.URL.Path[len(prefix):] - // support /cl/152700045/, which is used in commit 0edafefc36. - id = strings.TrimSuffix(id, "/") - if !validID.MatchString(id) { - http.Error(w, "Not found", http.StatusNotFound) - return - } - target := "" - - if n, err := strconv.Atoi(id); err == nil && isRietveldCL(n) { - // Issue 28836: if this Rietveld CL happens to - // also be a Gerrit CL, render a disambiguation HTML - // page with two links instead. We need to make a - // Gerrit API call to figure that out, but we cache - // known Gerrit CLs so it's done at most once per CL. - if ok, err := isGerritCL(r.Context(), n); err == nil && ok { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - clDisambiguationHTML.Execute(w, n) - return - } - - target = "https://codereview.appspot.com/" + id - } else { - target = "https://go-review.googlesource.com/" + id - } - http.Redirect(w, r, target, http.StatusFound) -} - -var clDisambiguationHTML = template.Must(template.New("").Parse(` - - - Go CL {{.}} Disambiguation - - - - CL number {{.}} exists in both Gerrit (the current code review system) - and Rietveld (the previous code review system). Please make a choice: - - - -`)) - -// isGerritCL reports whether a Gerrit CL with the specified numeric change ID (e.g., "4247") -// is known to exist by querying the Gerrit API at https://go-review.googlesource.com. -// isGerritCL uses gerritCLCache as a cache of Gerrit CL IDs that exist. -func isGerritCL(ctx context.Context, id int) (bool, error) { - // Check cache first. - gerritCLCache.Lock() - ok := gerritCLCache.exist[id] - gerritCLCache.Unlock() - if ok { - return true, nil - } - - // Query the Gerrit API Get Change endpoint, as documented at - // https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#get-change. - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - resp, err := ctxhttp.Get(ctx, nil, fmt.Sprintf("https://go-review.googlesource.com/changes/%d", id)) - if err != nil { - return false, err - } - resp.Body.Close() - switch resp.StatusCode { - case http.StatusOK: - // A Gerrit CL with this ID exists. Add it to cache. - gerritCLCache.Lock() - gerritCLCache.exist[id] = true - gerritCLCache.Unlock() - return true, nil - case http.StatusNotFound: - // A Gerrit CL with this ID doesn't exist. It may get created in the future. - return false, nil - default: - return false, fmt.Errorf("unexpected status code: %v", resp.Status) - } -} - -var gerritCLCache = struct { - sync.Mutex - exist map[int]bool // exist is a set of Gerrit CL IDs that are known to exist. -}{exist: make(map[int]bool)} - -var changeMap *hashMap - -// LoadChangeMap loads the specified map of Mercurial to Git revisions, -// which is used by the /change/ handler to intelligently map old hg -// revisions to their new git equivalents. -// It should be called before calling Register. -// The file should remain open as long as the process is running. -// See the implementation of this package for details. -func LoadChangeMap(filename string) error { - f, err := os.Open(filename) - if err != nil { - return err - } - m, err := newHashMap(f) - if err != nil { - return err - } - changeMap = m - return nil -} - -func changeHandler(w http.ResponseWriter, r *http.Request) { - const prefix = "/change/" - if p := r.URL.Path; p == prefix { - // redirect /prefix/ to /prefix - http.Redirect(w, r, p[:len(p)-1], http.StatusFound) - return - } - hash := r.URL.Path[len(prefix):] - target := "https://go.googlesource.com/go/+/" + hash - if git := changeMap.Lookup(hash); git > 0 { - target = fmt.Sprintf("https://go.googlesource.com/%v/+/%v", git.Repo(), git.Hash()) - } - http.Redirect(w, r, target, http.StatusFound) -} - -func designHandler(w http.ResponseWriter, r *http.Request) { - const prefix = "/design/" - if p := r.URL.Path; p == prefix { - // redirect /prefix/ to /prefix - http.Redirect(w, r, p[:len(p)-1], http.StatusFound) - return - } - name := r.URL.Path[len(prefix):] - target := "https://go.googlesource.com/proposal/+/master/design/" + name + ".md" - http.Redirect(w, r, target, http.StatusFound) -} diff --git a/godoc/redirect/redirect_test.go b/godoc/redirect/redirect_test.go index 1de3c6ca779..59677c435cc 100644 --- a/godoc/redirect/redirect_test.go +++ b/godoc/redirect/redirect_test.go @@ -21,56 +21,7 @@ func errorResult(status int) redirectResult { func TestRedirects(t *testing.T) { var tests = map[string]redirectResult{ - "/build": {301, "http://build.golang.org"}, - "/ref": {301, "/doc/#references"}, - "/doc/mem": {301, "/ref/mem"}, - "/doc/spec": {301, "/ref/spec"}, - "/tour": {301, "http://tour.golang.org"}, - "/foo": errorResult(404), - - "/pkg/asn1": {301, "/pkg/encoding/asn1/"}, - "/pkg/template/parse": {301, "/pkg/text/template/parse/"}, - - "/src/pkg/foo": {301, "/src/foo"}, - - "/cmd/gofix": {301, "/cmd/fix/"}, - - // git commits (/change) - // TODO: mercurial tags and LoadChangeMap. - "/change": {301, "https://go.googlesource.com/go"}, - "/change/a": {302, "https://go.googlesource.com/go/+/a"}, - - "/issue": {301, "https://github.com/golang/go/issues"}, - "/issue?": {301, "https://github.com/golang/go/issues"}, - "/issue/1": {302, "https://github.com/golang/go/issues/1"}, - "/issue/new": {301, "https://github.com/golang/go/issues/new"}, - "/issue/new?a=b&c=d%20&e=f": {301, "https://github.com/golang/go/issues/new?a=b&c=d%20&e=f"}, - "/issues": {301, "https://github.com/golang/go/issues"}, - "/issues/1": {302, "https://github.com/golang/go/issues/1"}, - "/issues/new": {301, "https://github.com/golang/go/issues/new"}, - "/issues/1/2/3": errorResult(404), - - "/wiki/foo": {302, "https://github.com/golang/go/wiki/foo"}, - "/wiki/foo/": {302, "https://github.com/golang/go/wiki/foo/"}, - - "/design": {301, "https://go.googlesource.com/proposal/+/master/design"}, - "/design/": {302, "/design"}, - "/design/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/123-foo.md"}, - "/design/text/123-foo": {302, "https://go.googlesource.com/proposal/+/master/design/text/123-foo.md"}, - - "/cl/1": {302, "https://go-review.googlesource.com/1"}, - "/cl/1/": {302, "https://go-review.googlesource.com/1"}, - "/cl/267120043": {302, "https://codereview.appspot.com/267120043"}, - "/cl/267120043/": {302, "https://codereview.appspot.com/267120043"}, - - // Verify that we're using the Rietveld CL table: - "/cl/152046": {302, "https://codereview.appspot.com/152046"}, - "/cl/152047": {302, "https://go-review.googlesource.com/152047"}, - "/cl/152048": {302, "https://codereview.appspot.com/152048"}, - - // And verify we're using the "bigEnoughAssumeRietveld" value: - "/cl/299999": {302, "https://go-review.googlesource.com/299999"}, - "/cl/300000": {302, "https://codereview.appspot.com/300000"}, + "/foo": errorResult(404), } mux := http.NewServeMux() diff --git a/godoc/redirect/rietveld.go b/godoc/redirect/rietveld.go deleted file mode 100644 index 81b1094db17..00000000000 --- a/godoc/redirect/rietveld.go +++ /dev/null @@ -1,1093 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package redirect - -// bigEnoughAssumeRietveld is the value where CLs equal or great are -// assumed to be on Rietveld. By including this threshold we shrink -// the size of the table below. When Go amasses 150,000 more CLs, we'll -// need to bump this number and regenerate the list below. -const bigEnoughAssumeRietveld = 300000 - -// isRietveldCL reports whether cl was a Rietveld CL number. -func isRietveldCL(cl int) bool { - return cl >= bigEnoughAssumeRietveld || lowRietveldCL[cl] -} - -// lowRietveldCLs are the old CL numbers assigned by Rietveld code -// review system as used by Go prior to Gerrit which are less than -// bigEnoughAssumeRietveld. -// -// This list of numbers is registered with the /cl/NNNN redirect -// handler to disambiguate which code review system a particular -// number corresponds to. In some rare cases there may be duplicates, -// in which case we might render an HTML choice for the user. -// -// To re-generate this list, run: -// -// $ cd $GOROOT -// $ git log 7d7c6a9..94151eb | grep "^ https://golang.org/cl/" | perl -ne 's,^\s+https://golang.org/cl/(\d+).*$,$1,; chomp; print "$_: true,\n" if $_ < 300000' | sort -n | uniq -// -// Note that we ignore the x/* repos because we didn't start using -// "subrepos" until the Rietveld CLs numbers were already 4,000,000+, -// well above bigEnoughAssumeRietveld. -var lowRietveldCL = map[int]bool{ - 152046: true, - 152048: true, - 152049: true, - 152050: true, - 152051: true, - 152052: true, - 152055: true, - 152056: true, - 152057: true, - 152072: true, - 152073: true, - 152075: true, - 152076: true, - 152077: true, - 152078: true, - 152079: true, - 152080: true, - 152082: true, - 152084: true, - 152085: true, - 152086: true, - 152088: true, - 152089: true, - 152091: true, - 152098: true, - 152101: true, - 152102: true, - 152105: true, - 152106: true, - 152107: true, - 152108: true, - 152109: true, - 152110: true, - 152114: true, - 152117: true, - 152118: true, - 152120: true, - 152123: true, - 152124: true, - 152128: true, - 152130: true, - 152131: true, - 152138: true, - 152141: true, - 152142: true, - 153048: true, - 153049: true, - 153050: true, - 153051: true, - 153055: true, - 153056: true, - 153057: true, - 154043: true, - 154044: true, - 154045: true, - 154049: true, - 154055: true, - 154057: true, - 154058: true, - 154059: true, - 154061: true, - 154064: true, - 154065: true, - 154067: true, - 154068: true, - 154069: true, - 154071: true, - 154072: true, - 154073: true, - 154076: true, - 154079: true, - 154096: true, - 154097: true, - 154099: true, - 154100: true, - 154101: true, - 154102: true, - 154108: true, - 154118: true, - 154121: true, - 154122: true, - 154123: true, - 154125: true, - 154126: true, - 154128: true, - 154136: true, - 154138: true, - 154139: true, - 154140: true, - 154141: true, - 154142: true, - 154143: true, - 154144: true, - 154145: true, - 154146: true, - 154152: true, - 154153: true, - 154156: true, - 154159: true, - 154161: true, - 154166: true, - 154167: true, - 154169: true, - 154171: true, - 154172: true, - 154173: true, - 154174: true, - 154175: true, - 154176: true, - 154177: true, - 154178: true, - 154179: true, - 154180: true, - 155041: true, - 155042: true, - 155045: true, - 155047: true, - 155048: true, - 155049: true, - 155050: true, - 155054: true, - 155055: true, - 155056: true, - 155057: true, - 155058: true, - 155059: true, - 155061: true, - 155062: true, - 155063: true, - 155065: true, - 155067: true, - 155069: true, - 155072: true, - 155074: true, - 155075: true, - 155077: true, - 155078: true, - 155079: true, - 156041: true, - 156044: true, - 156045: true, - 156046: true, - 156047: true, - 156051: true, - 156052: true, - 156054: true, - 156055: true, - 156056: true, - 156058: true, - 156059: true, - 156060: true, - 156061: true, - 156062: true, - 156063: true, - 156066: true, - 156067: true, - 156070: true, - 156071: true, - 156073: true, - 156075: true, - 156077: true, - 156079: true, - 156080: true, - 156081: true, - 156083: true, - 156084: true, - 156085: true, - 156086: true, - 156089: true, - 156091: true, - 156092: true, - 156093: true, - 156094: true, - 156097: true, - 156099: true, - 156100: true, - 156102: true, - 156103: true, - 156104: true, - 156106: true, - 156107: true, - 156108: true, - 156109: true, - 156110: true, - 156113: true, - 156115: true, - 156116: true, - 157041: true, - 157042: true, - 157043: true, - 157044: true, - 157046: true, - 157053: true, - 157055: true, - 157056: true, - 157058: true, - 157060: true, - 157061: true, - 157062: true, - 157065: true, - 157066: true, - 157067: true, - 157068: true, - 157069: true, - 157071: true, - 157072: true, - 157073: true, - 157074: true, - 157075: true, - 157076: true, - 157077: true, - 157082: true, - 157084: true, - 157085: true, - 157087: true, - 157088: true, - 157091: true, - 157095: true, - 157096: true, - 157099: true, - 157100: true, - 157101: true, - 157102: true, - 157103: true, - 157104: true, - 157106: true, - 157110: true, - 157111: true, - 157112: true, - 157114: true, - 157116: true, - 157119: true, - 157140: true, - 157142: true, - 157143: true, - 157144: true, - 157146: true, - 157147: true, - 157149: true, - 157151: true, - 157152: true, - 157153: true, - 157154: true, - 157156: true, - 157157: true, - 157158: true, - 157159: true, - 157160: true, - 157162: true, - 157166: true, - 157167: true, - 157168: true, - 157170: true, - 158041: true, - 159044: true, - 159049: true, - 159050: true, - 159051: true, - 160043: true, - 160044: true, - 160045: true, - 160046: true, - 160047: true, - 160054: true, - 160056: true, - 160057: true, - 160059: true, - 160060: true, - 160061: true, - 160064: true, - 160065: true, - 160069: true, - 160070: true, - 161049: true, - 161050: true, - 161056: true, - 161058: true, - 161060: true, - 161061: true, - 161069: true, - 161070: true, - 161073: true, - 161075: true, - 162041: true, - 162044: true, - 162046: true, - 162053: true, - 162054: true, - 162055: true, - 162056: true, - 162057: true, - 162058: true, - 162059: true, - 162061: true, - 162062: true, - 163042: true, - 163044: true, - 163049: true, - 163050: true, - 163051: true, - 163052: true, - 163053: true, - 163055: true, - 163058: true, - 163061: true, - 163062: true, - 163064: true, - 163067: true, - 163068: true, - 163069: true, - 163070: true, - 163071: true, - 163072: true, - 163082: true, - 163083: true, - 163085: true, - 163088: true, - 163091: true, - 163092: true, - 163097: true, - 163098: true, - 164043: true, - 164047: true, - 164049: true, - 164052: true, - 164053: true, - 164056: true, - 164059: true, - 164060: true, - 164062: true, - 164068: true, - 164069: true, - 164071: true, - 164073: true, - 164074: true, - 164075: true, - 164078: true, - 164079: true, - 164081: true, - 164082: true, - 164083: true, - 164085: true, - 164086: true, - 164088: true, - 164090: true, - 164091: true, - 164092: true, - 164093: true, - 164094: true, - 164095: true, - 165042: true, - 165044: true, - 165045: true, - 165048: true, - 165049: true, - 165050: true, - 165051: true, - 165055: true, - 165057: true, - 165058: true, - 165059: true, - 165061: true, - 165062: true, - 165063: true, - 165064: true, - 165065: true, - 165068: true, - 165070: true, - 165076: true, - 165078: true, - 165080: true, - 165083: true, - 165086: true, - 165097: true, - 165100: true, - 165101: true, - 166041: true, - 166043: true, - 166044: true, - 166047: true, - 166049: true, - 166052: true, - 166053: true, - 166055: true, - 166058: true, - 166059: true, - 166060: true, - 166064: true, - 166066: true, - 166067: true, - 166068: true, - 166070: true, - 166071: true, - 166072: true, - 166073: true, - 166074: true, - 166076: true, - 166077: true, - 166078: true, - 166080: true, - 167043: true, - 167044: true, - 167047: true, - 167050: true, - 167055: true, - 167057: true, - 167058: true, - 168041: true, - 168045: true, - 170042: true, - 170043: true, - 170044: true, - 170046: true, - 170047: true, - 170048: true, - 170049: true, - 171044: true, - 171046: true, - 171047: true, - 171048: true, - 171051: true, - 172041: true, - 172042: true, - 172043: true, - 172045: true, - 172049: true, - 173041: true, - 173044: true, - 173045: true, - 174042: true, - 174047: true, - 174048: true, - 174050: true, - 174051: true, - 174052: true, - 174053: true, - 174063: true, - 174064: true, - 174072: true, - 174076: true, - 174077: true, - 174078: true, - 174082: true, - 174083: true, - 174087: true, - 175045: true, - 175046: true, - 175047: true, - 175048: true, - 176056: true, - 176057: true, - 176058: true, - 176061: true, - 176062: true, - 176063: true, - 176064: true, - 176066: true, - 176067: true, - 176070: true, - 176071: true, - 176076: true, - 178043: true, - 178044: true, - 178046: true, - 178048: true, - 179047: true, - 179055: true, - 179061: true, - 179062: true, - 179063: true, - 179067: true, - 179069: true, - 179070: true, - 179072: true, - 179079: true, - 179088: true, - 179095: true, - 179096: true, - 179097: true, - 179099: true, - 179105: true, - 179106: true, - 179108: true, - 179118: true, - 179120: true, - 179125: true, - 179126: true, - 179128: true, - 179129: true, - 179130: true, - 180044: true, - 180045: true, - 180046: true, - 180047: true, - 180048: true, - 180049: true, - 180050: true, - 180052: true, - 180053: true, - 180054: true, - 180055: true, - 180056: true, - 180057: true, - 180059: true, - 180061: true, - 180064: true, - 180065: true, - 180068: true, - 180069: true, - 180070: true, - 180074: true, - 180075: true, - 180081: true, - 180082: true, - 180085: true, - 180092: true, - 180099: true, - 180105: true, - 180108: true, - 180112: true, - 180118: true, - 181041: true, - 181043: true, - 181044: true, - 181045: true, - 181049: true, - 181050: true, - 181055: true, - 181057: true, - 181058: true, - 181059: true, - 181063: true, - 181071: true, - 181073: true, - 181075: true, - 181077: true, - 181080: true, - 181083: true, - 181084: true, - 181085: true, - 181086: true, - 181087: true, - 181089: true, - 181097: true, - 181099: true, - 181102: true, - 181111: true, - 181130: true, - 181135: true, - 181137: true, - 181138: true, - 181139: true, - 181151: true, - 181152: true, - 181153: true, - 181155: true, - 181156: true, - 181157: true, - 181158: true, - 181160: true, - 181161: true, - 181163: true, - 181164: true, - 181171: true, - 181179: true, - 181183: true, - 181184: true, - 181186: true, - 182041: true, - 182043: true, - 182044: true, - 183042: true, - 183043: true, - 183044: true, - 183047: true, - 183049: true, - 183065: true, - 183066: true, - 183073: true, - 183074: true, - 183075: true, - 183083: true, - 183084: true, - 183087: true, - 183088: true, - 183090: true, - 183095: true, - 183104: true, - 183107: true, - 183109: true, - 183111: true, - 183112: true, - 183113: true, - 183116: true, - 183123: true, - 183124: true, - 183125: true, - 183126: true, - 183132: true, - 183133: true, - 183135: true, - 183136: true, - 183137: true, - 183138: true, - 183139: true, - 183140: true, - 183141: true, - 183142: true, - 183153: true, - 183155: true, - 183156: true, - 183157: true, - 183160: true, - 184043: true, - 184055: true, - 184058: true, - 184059: true, - 184068: true, - 184069: true, - 184079: true, - 184080: true, - 184081: true, - 185043: true, - 185045: true, - 186042: true, - 186043: true, - 186073: true, - 186076: true, - 186077: true, - 186078: true, - 186079: true, - 186081: true, - 186095: true, - 186108: true, - 186113: true, - 186115: true, - 186116: true, - 186118: true, - 186119: true, - 186132: true, - 186137: true, - 186138: true, - 186139: true, - 186143: true, - 186144: true, - 186145: true, - 186146: true, - 186147: true, - 186148: true, - 186159: true, - 186160: true, - 186161: true, - 186165: true, - 186169: true, - 186173: true, - 186180: true, - 186210: true, - 186211: true, - 186212: true, - 186213: true, - 186214: true, - 186215: true, - 186216: true, - 186228: true, - 186229: true, - 186230: true, - 186232: true, - 186234: true, - 186255: true, - 186263: true, - 186276: true, - 186279: true, - 186282: true, - 186283: true, - 188043: true, - 189042: true, - 189057: true, - 189059: true, - 189062: true, - 189078: true, - 189080: true, - 189083: true, - 189088: true, - 189093: true, - 189095: true, - 189096: true, - 189098: true, - 189100: true, - 190041: true, - 190042: true, - 190043: true, - 190044: true, - 190059: true, - 190062: true, - 190068: true, - 190074: true, - 190076: true, - 190077: true, - 190079: true, - 190085: true, - 190088: true, - 190103: true, - 190104: true, - 193055: true, - 193066: true, - 193067: true, - 193070: true, - 193075: true, - 193079: true, - 193080: true, - 193081: true, - 193091: true, - 193092: true, - 193095: true, - 193101: true, - 193104: true, - 194043: true, - 194045: true, - 194046: true, - 194050: true, - 194051: true, - 194052: true, - 194053: true, - 194064: true, - 194066: true, - 194069: true, - 194071: true, - 194072: true, - 194073: true, - 194074: true, - 194076: true, - 194077: true, - 194078: true, - 194082: true, - 194084: true, - 194085: true, - 194090: true, - 194091: true, - 194092: true, - 194094: true, - 194097: true, - 194098: true, - 194099: true, - 194100: true, - 194114: true, - 194116: true, - 194118: true, - 194119: true, - 194120: true, - 194121: true, - 194122: true, - 194126: true, - 194129: true, - 194131: true, - 194132: true, - 194133: true, - 194134: true, - 194146: true, - 194151: true, - 194156: true, - 194157: true, - 194159: true, - 194161: true, - 194165: true, - 195041: true, - 195044: true, - 195050: true, - 195051: true, - 195052: true, - 195068: true, - 195075: true, - 195076: true, - 195079: true, - 195080: true, - 195081: true, - 196042: true, - 196044: true, - 196050: true, - 196051: true, - 196055: true, - 196056: true, - 196061: true, - 196063: true, - 196065: true, - 196070: true, - 196071: true, - 196075: true, - 196077: true, - 196079: true, - 196087: true, - 196088: true, - 196090: true, - 196091: true, - 197041: true, - 197042: true, - 197043: true, - 197044: true, - 198044: true, - 198045: true, - 198046: true, - 198048: true, - 198049: true, - 198050: true, - 198053: true, - 198057: true, - 198058: true, - 198066: true, - 198071: true, - 198074: true, - 198081: true, - 198084: true, - 198085: true, - 198102: true, - 199042: true, - 199044: true, - 199045: true, - 199046: true, - 199047: true, - 199052: true, - 199054: true, - 199057: true, - 199066: true, - 199070: true, - 199082: true, - 199091: true, - 199094: true, - 199096: true, - 201041: true, - 201042: true, - 201043: true, - 201047: true, - 201048: true, - 201049: true, - 201058: true, - 201061: true, - 201064: true, - 201065: true, - 201068: true, - 202042: true, - 202043: true, - 202044: true, - 202051: true, - 202054: true, - 202055: true, - 203043: true, - 203050: true, - 203051: true, - 203053: true, - 203060: true, - 203062: true, - 204042: true, - 204044: true, - 204048: true, - 204052: true, - 204053: true, - 204061: true, - 204062: true, - 204064: true, - 204065: true, - 204067: true, - 204068: true, - 204069: true, - 205042: true, - 205044: true, - 206043: true, - 206044: true, - 206047: true, - 206050: true, - 206051: true, - 206052: true, - 206053: true, - 206054: true, - 206055: true, - 206058: true, - 206059: true, - 206060: true, - 206067: true, - 206069: true, - 206077: true, - 206078: true, - 206079: true, - 206084: true, - 206089: true, - 206101: true, - 206107: true, - 206109: true, - 207043: true, - 207044: true, - 207049: true, - 207050: true, - 207051: true, - 207052: true, - 207053: true, - 207054: true, - 207055: true, - 207061: true, - 207062: true, - 207069: true, - 207071: true, - 207085: true, - 207086: true, - 207087: true, - 207088: true, - 207095: true, - 207096: true, - 207102: true, - 207103: true, - 207106: true, - 207108: true, - 207110: true, - 207111: true, - 207112: true, - 209041: true, - 209042: true, - 209043: true, - 209044: true, - 210042: true, - 210043: true, - 210044: true, - 210047: true, - 211041: true, - 212041: true, - 212045: true, - 212046: true, - 212047: true, - 213041: true, - 213042: true, - 214042: true, - 214046: true, - 214049: true, - 214050: true, - 215042: true, - 215048: true, - 215050: true, - 216043: true, - 216046: true, - 216047: true, - 216052: true, - 216053: true, - 216054: true, - 216059: true, - 216068: true, - 217041: true, - 217044: true, - 217047: true, - 217048: true, - 217049: true, - 217056: true, - 217058: true, - 217059: true, - 217060: true, - 217061: true, - 217064: true, - 217066: true, - 217069: true, - 217071: true, - 217085: true, - 217086: true, - 217088: true, - 217093: true, - 217094: true, - 217108: true, - 217109: true, - 217111: true, - 217115: true, - 217116: true, - 218042: true, - 218044: true, - 218046: true, - 218050: true, - 218060: true, - 218061: true, - 218063: true, - 218064: true, - 218065: true, - 218070: true, - 218071: true, - 218072: true, - 218074: true, - 218076: true, - 222041: true, - 223041: true, - 223043: true, - 223044: true, - 223050: true, - 223052: true, - 223054: true, - 223058: true, - 223059: true, - 223061: true, - 223068: true, - 223069: true, - 223070: true, - 223071: true, - 223073: true, - 223075: true, - 223076: true, - 223083: true, - 223087: true, - 223094: true, - 223096: true, - 223101: true, - 223106: true, - 223108: true, - 224041: true, - 224042: true, - 224043: true, - 224045: true, - 224051: true, - 224053: true, - 224057: true, - 224060: true, - 224061: true, - 224062: true, - 224063: true, - 224068: true, - 224069: true, - 224081: true, - 224084: true, - 224087: true, - 224090: true, - 224096: true, - 224105: true, - 225042: true, - 227041: true, - 229045: true, - 229046: true, - 229048: true, - 229049: true, - 229050: true, - 231042: true, - 236041: true, - 237041: true, - 238041: true, - 238042: true, - 240041: true, - 240042: true, - 240043: true, - 241041: true, - 243041: true, - 244041: true, - 245041: true, - 247041: true, - 250041: true, - 252041: true, - 253041: true, - 253045: true, - 254043: true, - 255042: true, - 255043: true, - 257041: true, - 257042: true, - 258041: true, - 261041: true, - 264041: true, - 294042: true, - 296042: true, -} diff --git a/godoc/static/package.html b/godoc/static/package.html index 86445df4c08..a04b08b63f5 100644 --- a/godoc/static/package.html +++ b/godoc/static/package.html @@ -17,7 +17,7 @@ {{if $.IsMain}} {{/* command documentation */}} - {{comment_html .Doc}} + {{comment_html $ .Doc}} {{else}} {{/* package documentation */}}
@@ -42,7 +42,7 @@

Overview ▹

Overview ▾

- {{comment_html .Doc}} + {{comment_html $ .Doc}} {{example_html $ ""}}
@@ -154,14 +154,14 @@

Inter {{with .Consts}}

Constants

{{range .}} - {{comment_html .Doc}} + {{comment_html $ .Doc}}
{{node_html $ .Decl true}}
{{end}} {{end}} {{with .Vars}}

Variables

{{range .}} - {{comment_html .Doc}} + {{comment_html $ .Doc}}
{{node_html $ .Decl true}}
{{end}} {{end}} @@ -174,7 +174,7 @@

func {{$name_html}}{{$since}}{{end}}

{{node_html $ .Decl true}}
- {{comment_html .Doc}} + {{comment_html $ .Doc}} {{example_html $ .Name}} {{callgraph_html $ "" .Name}} @@ -187,16 +187,16 @@

type {{$tname_html}}< {{$since := since "type" "" .Name $.PDoc.ImportPath}} {{if $since}}{{$since}}{{end}}

- {{comment_html .Doc}} + {{comment_html $ .Doc}}
{{node_html $ .Decl true}}
{{range .Consts}} - {{comment_html .Doc}} + {{comment_html $ .Doc}}
{{node_html $ .Decl true}}
{{end}} {{range .Vars}} - {{comment_html .Doc}} + {{comment_html $ .Doc}}
{{node_html $ .Decl true}}
{{end}} @@ -212,7 +212,7 @@

func {{$name_html}}{{$since}}{{end}}

{{node_html $ .Decl true}}
- {{comment_html .Doc}} + {{comment_html $ .Doc}} {{example_html $ .Name}} {{callgraph_html $ "" .Name}} {{end}} @@ -225,7 +225,7 @@

func ({{html .Recv}}) {{$since}}{{end}}

{{node_html $ .Decl true}}
- {{comment_html .Doc}} + {{comment_html $ .Doc}} {{$name := printf "%s_%s" $tname .Name}} {{example_html $ $name}} {{callgraph_html $ .Recv .Name}} @@ -238,7 +238,7 @@

func ({{html .Recv}}) {{noteTitle $marker | html}}s

{{end}} diff --git a/godoc/static/searchdoc.html b/godoc/static/searchdoc.html index 679c02cf3a8..84dcb345270 100644 --- a/godoc/static/searchdoc.html +++ b/godoc/static/searchdoc.html @@ -15,7 +15,7 @@

{{$key.Name}}

{{html .Package}}.{{.Name}} {{end}} {{if .Doc}} -

{{comment_html .Doc}}

+

{{comment_html $ .Doc}}

{{else}}

No documentation available

{{end}} diff --git a/godoc/static/static.go b/godoc/static/static.go index ada60fab6c2..d6e5f2d2e0e 100644 --- a/godoc/static/static.go +++ b/godoc/static/static.go @@ -83,7 +83,7 @@ var Files = map[string]string{ "methodset.html": "\x0a\x09\x0a\x09\x09\xe2\x96\xb9\x20Method\x20set

\x0a\x09\x0a\x09\x0a\x09\x09\xe2\x96\xbe\x20Method\x20set

\x0a\x09\x09...\x0a\x09\x0a\x0a", - "package.html": "\x0a\x0a{{with\x20.PDoc}}\x0a\x09\x0a\x0a\x09{{if\x20$.IsMain}}\x0a\x09\x09{{/*\x20command\x20documentation\x20*/}}\x0a\x09\x09{{comment_html\x20.Doc}}\x0a\x09{{else}}\x0a\x09\x09{{/*\x20package\x20documentation\x20*/}}\x0a\x09\x09\x0a\x09\x09\x09
\x0a\x09\x09\x09
import\x20\"{{html\x20.ImportPath}}\"
\x0a\x09\x09\x09
\x0a\x09\x09\x09
\x0a\x09\x09\x09
Overview
\x0a\x09\x09\x09
Index
\x0a\x09\x09\x09{{if\x20$.Examples}}\x0a\x09\x09\x09\x09
Examples
\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{if\x20$.Dirs}}\x0a\x09\x09\x09\x09
Subdirectories
\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09
\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Overview\x20\xe2\x96\xb9\x0a\x09\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Overview\x20\xe2\x96\xbe\x0a\x09\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09\x09{{example_html\x20$\x20\"\"}}\x0a\x09\x09\x09\x0a\x09\x09\x0a\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x09Index\x20\xe2\x96\xb9\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x09Index\x20\xe2\x96\xbe\x0a\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09
\x0a\x09\x09\x09{{if\x20.Consts}}\x0a\x09\x09\x09\x09
Constants
\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{if\x20.Vars}}\x0a\x09\x09\x09\x09
Variables
\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{range\x20.Funcs}}\x0a\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09
{{node_html\x20$\x20.Decl\x20false\x20|\x20sanitize}}
\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{range\x20.Types}}\x0a\x09\x09\x09\x09{{$tname_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09
type\x20{{$tname_html}}
\x0a\x09\x09\x09\x09{{range\x20.Funcs}}\x0a\x09\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09\x09
 \x20 \x20{{node_html\x20$\x20.Decl\x20false\x20|\x20sanitize}}
\x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09{{range\x20.Methods}}\x0a\x09\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09\x09
 \x20 \x20{{node_html\x20$\x20.Decl\x20false\x20|\x20sanitize}}
\x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{if\x20$.Notes}}\x0a\x09\x09\x09\x09{{range\x20$marker,\x20$item\x20:=\x20$.Notes}}\x0a\x09\x09\x09\x09
{{noteTitle\x20$marker\x20|\x20html}}s
\x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09
\x0a\x09\x09\x09\x0a\x0a\x09\x09{{if\x20$.Examples}}\x0a\x09\x09\x0a\x09\x09\x09

Examples

\x0a\x09\x09\x09(Expand\x20All)\x0a\x09\x09\x09
\x0a\x09\x09\x09{{range\x20$.Examples}}\x0a\x09\x09\x09
{{example_name\x20.Name}}
\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09
\x0a\x09\x09\x0a\x09\x09{{end}}\x0a\x0a\x09\x09{{with\x20.Filenames}}\x0a\x09\x09\x09

Package\x20files

\x0a\x09\x09\x09

\x0a\x09\x09\x09\x0a\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09\x09{{.|filename|html}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09\x0a\x09\x09\x09

\x0a\x09\x09{{end}}\x0a\x09\x09\x0a\x09\x09\x0a\x0a\x09\x09{{if\x20ne\x20$.CallGraph\x20\"null\"}}\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x09Internal\x20call\x20graph\x20\xe2\x96\xb9\x0a\x09\x09\x20\x0a\x09\x09\x0a\x09\x09\x09Internal\x20call\x20graph\x20\xe2\x96\xbe\x0a\x09\x09\x09

\x0a\x09\x09\x09\x20\x20In\x20the\x20call\x20graph\x20viewer\x20below,\x20each\x20node\x0a\x09\x09\x09\x20\x20is\x20a\x20function\x20belonging\x20to\x20this\x20package\x0a\x09\x09\x09\x20\x20and\x20its\x20children\x20are\x20the\x20functions\x20it\x0a\x09\x09\x09\x20\x20calls—perhaps\x20dynamically.\x0a\x09\x09\x09

\x0a\x09\x09\x09

\x0a\x09\x09\x09\x20\x20The\x20root\x20nodes\x20are\x20the\x20entry\x20points\x20of\x20the\x0a\x09\x09\x09\x20\x20package:\x20functions\x20that\x20may\x20be\x20called\x20from\x0a\x09\x09\x09\x20\x20outside\x20the\x20package.\x0a\x09\x09\x09\x20\x20There\x20may\x20be\x20non-exported\x20or\x20anonymous\x0a\x09\x09\x09\x20\x20functions\x20among\x20them\x20if\x20they\x20are\x20called\x0a\x09\x09\x09\x20\x20dynamically\x20from\x20another\x20package.\x0a\x09\x09\x09

\x0a\x09\x09\x09

\x0a\x09\x09\x09\x20\x20Click\x20a\x20node\x20to\x20visit\x20that\x20function's\x20source\x20code.\x0a\x09\x09\x09\x20\x20From\x20there\x20you\x20can\x20visit\x20its\x20callers\x20by\x0a\x09\x09\x09\x20\x20clicking\x20its\x20declaring\x20func\x0a\x09\x09\x09\x20\x20token.\x0a\x09\x09\x09

\x0a\x09\x09\x09

\x0a\x09\x09\x09\x20\x20Functions\x20may\x20be\x20omitted\x20if\x20they\x20were\x0a\x09\x09\x09\x20\x20determined\x20to\x20be\x20unreachable\x20in\x20the\x0a\x09\x09\x09\x20\x20particular\x20programs\x20or\x20tests\x20that\x20were\x0a\x09\x09\x09\x20\x20analyzed.\x0a\x09\x09\x09

\x0a\x09\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x0a\x09\x09\x20\x0a\x09\x09{{end}}\x0a\x0a\x09\x09{{with\x20.Consts}}\x0a\x09\x09\x09Constants\x0a\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09\x09
{{node_html\x20$\x20.Decl\x20true}}
\x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09\x09{{with\x20.Vars}}\x0a\x09\x09\x09Variables\x0a\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09\x09
{{node_html\x20$\x20.Decl\x20true}}
\x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09\x09{{range\x20.Funcs}}\x0a\x09\x09\x09{{/*\x20Name\x20is\x20a\x20string\x20-\x20no\x20need\x20for\x20FSet\x20*/}}\x0a\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09func\x20{{$name_html}}\x0a\x09\x09\x09\x09¶\x0a\x09\x09\x09\x09{{$since\x20:=\x20since\x20\"func\"\x20\"\"\x20.Name\x20$.PDoc.ImportPath}}\x0a\x09\x09\x09\x09{{if\x20$since}}{{$since}}{{end}}\x0a\x09\x09\x09\x0a\x09\x09\x09
{{node_html\x20$\x20.Decl\x20true}}
\x0a\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09{{example_html\x20$\x20.Name}}\x0a\x09\x09\x09{{callgraph_html\x20$\x20\"\"\x20.Name}}\x0a\x0a\x09\x09{{end}}\x0a\x09\x09{{range\x20.Types}}\x0a\x09\x09\x09{{$tname\x20:=\x20.Name}}\x0a\x09\x09\x09{{$tname_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09type\x20{{$tname_html}}\x0a\x09\x09\x09\x09¶\x0a\x09\x09\x09\x09{{$since\x20:=\x20since\x20\"type\"\x20\"\"\x20.Name\x20$.PDoc.ImportPath}}\x0a\x09\x09\x09\x09{{if\x20$since}}{{$since}}{{end}}\x0a\x09\x09\x09\x0a\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09
{{node_html\x20$\x20.Decl\x20true}}
\x0a\x0a\x09\x09\x09{{range\x20.Consts}}\x0a\x09\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09\x09
{{node_html\x20$\x20.Decl\x20true}}
\x0a\x09\x09\x09{{end}}\x0a\x0a\x09\x09\x09{{range\x20.Vars}}\x0a\x09\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09\x09
{{node_html\x20$\x20.Decl\x20true}}
\x0a\x09\x09\x09{{end}}\x0a\x0a\x09\x09\x09{{example_html\x20$\x20$tname}}\x0a\x09\x09\x09{{implements_html\x20$\x20$tname}}\x0a\x09\x09\x09{{methodset_html\x20$\x20$tname}}\x0a\x0a\x09\x09\x09{{range\x20.Funcs}}\x0a\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09func\x20{{$name_html}}\x0a\x09\x09\x09\x09\x09¶\x0a\x09\x09\x09\x09\x09{{$since\x20:=\x20since\x20\"func\"\x20\"\"\x20.Name\x20$.PDoc.ImportPath}}\x0a\x09\x09\x09\x09\x09{{if\x20$since}}{{$since}}{{end}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09
{{node_html\x20$\x20.Decl\x20true}}
\x0a\x09\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09\x09{{example_html\x20$\x20.Name}}\x0a\x09\x09\x09\x09{{callgraph_html\x20$\x20\"\"\x20.Name}}\x0a\x09\x09\x09{{end}}\x0a\x0a\x09\x09\x09{{range\x20.Methods}}\x0a\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09func\x20({{html\x20.Recv}})\x20{{$name_html}}\x0a\x09\x09\x09\x09\x09¶\x0a\x09\x09\x09\x09\x09{{$since\x20:=\x20since\x20\"method\"\x20.Recv\x20.Name\x20$.PDoc.ImportPath}}\x0a\x09\x09\x09\x09\x09{{if\x20$since}}{{$since}}{{end}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09
{{node_html\x20$\x20.Decl\x20true}}
\x0a\x09\x09\x09\x09{{comment_html\x20.Doc}}\x0a\x09\x09\x09\x09{{$name\x20:=\x20printf\x20\"%s_%s\"\x20$tname\x20.Name}}\x0a\x09\x09\x09\x09{{example_html\x20$\x20$name}}\x0a\x09\x09\x09\x09{{callgraph_html\x20$\x20.Recv\x20.Name}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09{{end}}\x0a\x0a\x09{{with\x20$.Notes}}\x0a\x09\x09{{range\x20$marker,\x20$content\x20:=\x20.}}\x0a\x09\x09\x09{{noteTitle\x20$marker\x20|\x20html}}s\x0a\x09\x09\x09\x0a\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09
  • ☞\x20{{comment_html\x20.Body}}
  • \x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09\x0a\x09\x09{{end}}\x0a\x09{{end}}\x0a{{end}}\x0a\x0a{{with\x20.PAst}}\x0a\x09{{range\x20$filename,\x20$ast\x20:=\x20.}}\x0a\x09\x09{{$filename|filename|html}}:
    {{node_html\x20$\x20$ast\x20false}}
    \x0a\x09{{end}}\x0a{{end}}\x0a\x0a{{with\x20.Dirs}}\x0a\x09{{/*\x20DirList\x20entries\x20are\x20numbers\x20and\x20strings\x20-\x20no\x20need\x20for\x20FSet\x20*/}}\x0a\x09{{if\x20$.PDoc}}\x0a\x09\x09Subdirectories\x0a\x09{{end}}\x0a\x09\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Name\x0a\x09\x09\x09\x09Synopsis\x0a\x09\x09\x09\x0a\x0a\x09\x09\x09{{if\x20not\x20(or\x20(eq\x20$.Dirname\x20\"/src/cmd\")\x20$.DirFlat)}}\x0a\x09\x09\x09\x0a\x09\x09\x09\x09..\x0a\x09\x09\x09\x0a\x09\x09\x09{{end}}\x0a\x0a\x09\x09\x09{{range\x20.List}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09{{if\x20$.DirFlat}}\x0a\x09\x09\x09\x09\x09{{if\x20.HasPkg}}\x0a\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09{{html\x20.Path}}\x0a\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09{{else}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09{{html\x20.Name}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09{{html\x20.Synopsis}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x0a\x09\x09\x09{{end}}\x0a\x09\x09
    \x0a\x09\x0a{{end}}\x0a", + "package.html": "\x0a\x0a{{with\x20.PDoc}}\x0a\x09\x0a\x0a\x09{{if\x20$.IsMain}}\x0a\x09\x09{{/*\x20command\x20documentation\x20*/}}\x0a\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09{{else}}\x0a\x09\x09{{/*\x20package\x20documentation\x20*/}}\x0a\x09\x09\x0a\x09\x09\x09
    \x0a\x09\x09\x09
    import\x20\"{{html\x20.ImportPath}}\"
    \x0a\x09\x09\x09
    \x0a\x09\x09\x09
    \x0a\x09\x09\x09
    Overview
    \x0a\x09\x09\x09
    Index
    \x0a\x09\x09\x09{{if\x20$.Examples}}\x0a\x09\x09\x09\x09
    Examples
    \x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{if\x20$.Dirs}}\x0a\x09\x09\x09\x09
    Subdirectories
    \x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09
    \x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Overview\x20\xe2\x96\xb9\x0a\x09\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Overview\x20\xe2\x96\xbe\x0a\x09\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09\x09{{example_html\x20$\x20\"\"}}\x0a\x09\x09\x09\x0a\x09\x09\x0a\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x09Index\x20\xe2\x96\xb9\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x09Index\x20\xe2\x96\xbe\x0a\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09
    \x0a\x09\x09\x09{{if\x20.Consts}}\x0a\x09\x09\x09\x09
    Constants
    \x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{if\x20.Vars}}\x0a\x09\x09\x09\x09
    Variables
    \x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{range\x20.Funcs}}\x0a\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20false\x20|\x20sanitize}}
    \x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{range\x20.Types}}\x0a\x09\x09\x09\x09{{$tname_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09
    type\x20{{$tname_html}}
    \x0a\x09\x09\x09\x09{{range\x20.Funcs}}\x0a\x09\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09\x09
     \x20 \x20{{node_html\x20$\x20.Decl\x20false\x20|\x20sanitize}}
    \x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09{{range\x20.Methods}}\x0a\x09\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09\x09
     \x20 \x20{{node_html\x20$\x20.Decl\x20false\x20|\x20sanitize}}
    \x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{if\x20$.Notes}}\x0a\x09\x09\x09\x09{{range\x20$marker,\x20$item\x20:=\x20$.Notes}}\x0a\x09\x09\x09\x09
    {{noteTitle\x20$marker\x20|\x20html}}s
    \x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09
    \x0a\x09\x09\x09\x0a\x0a\x09\x09{{if\x20$.Examples}}\x0a\x09\x09\x0a\x09\x09\x09

    Examples

    \x0a\x09\x09\x09(Expand\x20All)\x0a\x09\x09\x09
    \x0a\x09\x09\x09{{range\x20$.Examples}}\x0a\x09\x09\x09
    {{example_name\x20.Name}}
    \x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09
    \x0a\x09\x09\x0a\x09\x09{{end}}\x0a\x0a\x09\x09{{with\x20.Filenames}}\x0a\x09\x09\x09

    Package\x20files

    \x0a\x09\x09\x09

    \x0a\x09\x09\x09\x0a\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09\x09{{.|filename|html}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09\x0a\x09\x09\x09

    \x0a\x09\x09{{end}}\x0a\x09\x09\x0a\x09\x09\x0a\x0a\x09\x09{{if\x20ne\x20$.CallGraph\x20\"null\"}}\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09\x09Internal\x20call\x20graph\x20\xe2\x96\xb9\x0a\x09\x09\x20\x0a\x09\x09\x0a\x09\x09\x09Internal\x20call\x20graph\x20\xe2\x96\xbe\x0a\x09\x09\x09

    \x0a\x09\x09\x09\x20\x20In\x20the\x20call\x20graph\x20viewer\x20below,\x20each\x20node\x0a\x09\x09\x09\x20\x20is\x20a\x20function\x20belonging\x20to\x20this\x20package\x0a\x09\x09\x09\x20\x20and\x20its\x20children\x20are\x20the\x20functions\x20it\x0a\x09\x09\x09\x20\x20calls—perhaps\x20dynamically.\x0a\x09\x09\x09

    \x0a\x09\x09\x09

    \x0a\x09\x09\x09\x20\x20The\x20root\x20nodes\x20are\x20the\x20entry\x20points\x20of\x20the\x0a\x09\x09\x09\x20\x20package:\x20functions\x20that\x20may\x20be\x20called\x20from\x0a\x09\x09\x09\x20\x20outside\x20the\x20package.\x0a\x09\x09\x09\x20\x20There\x20may\x20be\x20non-exported\x20or\x20anonymous\x0a\x09\x09\x09\x20\x20functions\x20among\x20them\x20if\x20they\x20are\x20called\x0a\x09\x09\x09\x20\x20dynamically\x20from\x20another\x20package.\x0a\x09\x09\x09

    \x0a\x09\x09\x09

    \x0a\x09\x09\x09\x20\x20Click\x20a\x20node\x20to\x20visit\x20that\x20function's\x20source\x20code.\x0a\x09\x09\x09\x20\x20From\x20there\x20you\x20can\x20visit\x20its\x20callers\x20by\x0a\x09\x09\x09\x20\x20clicking\x20its\x20declaring\x20func\x0a\x09\x09\x09\x20\x20token.\x0a\x09\x09\x09

    \x0a\x09\x09\x09

    \x0a\x09\x09\x09\x20\x20Functions\x20may\x20be\x20omitted\x20if\x20they\x20were\x0a\x09\x09\x09\x20\x20determined\x20to\x20be\x20unreachable\x20in\x20the\x0a\x09\x09\x09\x20\x20particular\x20programs\x20or\x20tests\x20that\x20were\x0a\x09\x09\x09\x20\x20analyzed.\x0a\x09\x09\x09

    \x0a\x09\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x0a\x09\x09\x20\x0a\x09\x09{{end}}\x0a\x0a\x09\x09{{with\x20.Consts}}\x0a\x09\x09\x09Constants\x0a\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20true}}
    \x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09\x09{{with\x20.Vars}}\x0a\x09\x09\x09Variables\x0a\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20true}}
    \x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09\x09{{range\x20.Funcs}}\x0a\x09\x09\x09{{/*\x20Name\x20is\x20a\x20string\x20-\x20no\x20need\x20for\x20FSet\x20*/}}\x0a\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09func\x20{{$name_html}}\x0a\x09\x09\x09\x09¶\x0a\x09\x09\x09\x09{{$since\x20:=\x20since\x20\"func\"\x20\"\"\x20.Name\x20$.PDoc.ImportPath}}\x0a\x09\x09\x09\x09{{if\x20$since}}{{$since}}{{end}}\x0a\x09\x09\x09\x0a\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20true}}
    \x0a\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09{{example_html\x20$\x20.Name}}\x0a\x09\x09\x09{{callgraph_html\x20$\x20\"\"\x20.Name}}\x0a\x0a\x09\x09{{end}}\x0a\x09\x09{{range\x20.Types}}\x0a\x09\x09\x09{{$tname\x20:=\x20.Name}}\x0a\x09\x09\x09{{$tname_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09type\x20{{$tname_html}}\x0a\x09\x09\x09\x09¶\x0a\x09\x09\x09\x09{{$since\x20:=\x20since\x20\"type\"\x20\"\"\x20.Name\x20$.PDoc.ImportPath}}\x0a\x09\x09\x09\x09{{if\x20$since}}{{$since}}{{end}}\x0a\x09\x09\x09\x0a\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20true}}
    \x0a\x0a\x09\x09\x09{{range\x20.Consts}}\x0a\x09\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20true}}
    \x0a\x09\x09\x09{{end}}\x0a\x0a\x09\x09\x09{{range\x20.Vars}}\x0a\x09\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20true}}
    \x0a\x09\x09\x09{{end}}\x0a\x0a\x09\x09\x09{{example_html\x20$\x20$tname}}\x0a\x09\x09\x09{{implements_html\x20$\x20$tname}}\x0a\x09\x09\x09{{methodset_html\x20$\x20$tname}}\x0a\x0a\x09\x09\x09{{range\x20.Funcs}}\x0a\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09func\x20{{$name_html}}\x0a\x09\x09\x09\x09\x09¶\x0a\x09\x09\x09\x09\x09{{$since\x20:=\x20since\x20\"func\"\x20\"\"\x20.Name\x20$.PDoc.ImportPath}}\x0a\x09\x09\x09\x09\x09{{if\x20$since}}{{$since}}{{end}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20true}}
    \x0a\x09\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09\x09{{example_html\x20$\x20.Name}}\x0a\x09\x09\x09\x09{{callgraph_html\x20$\x20\"\"\x20.Name}}\x0a\x09\x09\x09{{end}}\x0a\x0a\x09\x09\x09{{range\x20.Methods}}\x0a\x09\x09\x09\x09{{$name_html\x20:=\x20html\x20.Name}}\x0a\x09\x09\x09\x09func\x20({{html\x20.Recv}})\x20{{$name_html}}\x0a\x09\x09\x09\x09\x09¶\x0a\x09\x09\x09\x09\x09{{$since\x20:=\x20since\x20\"method\"\x20.Recv\x20.Name\x20$.PDoc.ImportPath}}\x0a\x09\x09\x09\x09\x09{{if\x20$since}}{{$since}}{{end}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09
    {{node_html\x20$\x20.Decl\x20true}}
    \x0a\x09\x09\x09\x09{{comment_html\x20$\x20.Doc}}\x0a\x09\x09\x09\x09{{$name\x20:=\x20printf\x20\"%s_%s\"\x20$tname\x20.Name}}\x0a\x09\x09\x09\x09{{example_html\x20$\x20$name}}\x0a\x09\x09\x09\x09{{callgraph_html\x20$\x20.Recv\x20.Name}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09{{end}}\x0a\x0a\x09{{with\x20$.Notes}}\x0a\x09\x09{{range\x20$marker,\x20$content\x20:=\x20.}}\x0a\x09\x09\x09{{noteTitle\x20$marker\x20|\x20html}}s\x0a\x09\x09\x09\x0a\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09
  • ☞\x20{{comment_html\x20$\x20.Body}}
  • \x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09\x0a\x09\x09{{end}}\x0a\x09{{end}}\x0a{{end}}\x0a\x0a{{with\x20.PAst}}\x0a\x09{{range\x20$filename,\x20$ast\x20:=\x20.}}\x0a\x09\x09{{$filename|filename|html}}:
    {{node_html\x20$\x20$ast\x20false}}
    \x0a\x09{{end}}\x0a{{end}}\x0a\x0a{{with\x20.Dirs}}\x0a\x09{{/*\x20DirList\x20entries\x20are\x20numbers\x20and\x20strings\x20-\x20no\x20need\x20for\x20FSet\x20*/}}\x0a\x09{{if\x20$.PDoc}}\x0a\x09\x09Subdirectories\x0a\x09{{end}}\x0a\x09\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Name\x0a\x09\x09\x09\x09Synopsis\x0a\x09\x09\x09\x0a\x0a\x09\x09\x09{{if\x20not\x20(or\x20(eq\x20$.Dirname\x20\"/src/cmd\")\x20$.DirFlat)}}\x0a\x09\x09\x09\x0a\x09\x09\x09\x09..\x0a\x09\x09\x09\x0a\x09\x09\x09{{end}}\x0a\x0a\x09\x09\x09{{range\x20.List}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09{{if\x20$.DirFlat}}\x0a\x09\x09\x09\x09\x09{{if\x20.HasPkg}}\x0a\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09{{html\x20.Path}}\x0a\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09{{else}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09{{html\x20.Name}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09{{html\x20.Synopsis}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x0a\x09\x09\x09{{end}}\x0a\x09\x09
    \x0a\x09\x0a{{end}}\x0a", "packageroot.html": "\x0a\x0a{{with\x20.PAst}}\x0a\x09{{range\x20$filename,\x20$ast\x20:=\x20.}}\x0a\x09\x09{{$filename|filename|html}}:
    {{node_html\x20$\x20$ast\x20false}}
    \x0a\x09{{end}}\x0a{{end}}\x0a\x0a{{with\x20.Dirs}}\x0a\x09{{/*\x20DirList\x20entries\x20are\x20numbers\x20and\x20strings\x20-\x20no\x20need\x20for\x20FSet\x20*/}}\x0a\x09{{if\x20$.PDoc}}\x0a\x09\x09Subdirectories\x0a\x09{{end}}\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09
    \x0a\x09\x09\x09\x09
    Standard\x20library
    \x0a\x09\x09\x09\x09{{if\x20hasThirdParty\x20.List\x20}}\x0a\x09\x09\x09\x09\x09
    Third\x20party
    \x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09
    Other\x20packages
    \x0a\x09\x09\x09\x09
    Sub-repositories
    \x0a\x09\x09\x09\x09
    Community
    \x0a\x09\x09\x09
    \x0a\x09\x09\x0a\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Standard\x20library\x20\xe2\x96\xb9\x0a\x09\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Standard\x20library\x20\xe2\x96\xbe\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09Name\x0a\x09\x09\x09\x09\x09\x09\x09Synopsis\x0a\x09\x09\x09\x09\x09\x09\x0a\x0a\x09\x09\x09\x09\x09\x09{{range\x20.List}}\x0a\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09{{if\x20eq\x20.RootType\x20\"GOROOT\"}}\x0a\x09\x09\x09\x09\x09\x09\x09{{if\x20$.DirFlat}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09{{if\x20.HasPkg}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09{{html\x20.Path}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x09\x09{{else}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09{{html\x20.Name}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09{{html\x20.Synopsis}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09
    \x0a\x09\x09\x09\x09\x20\x0a\x09\x09\x09\x20\x0a\x09\x09\x20\x0a\x0a\x09{{if\x20hasThirdParty\x20.List\x20}}\x0a\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Third\x20party\x20\xe2\x96\xb9\x0a\x09\x09\x09\x0a\x09\x09\x09\x0a\x09\x09\x09\x09Third\x20party\x20\xe2\x96\xbe\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09Name\x0a\x09\x09\x09\x09\x09\x09\x09Synopsis\x0a\x09\x09\x09\x09\x09\x09\x0a\x0a\x09\x09\x09\x09\x09\x09{{range\x20.List}}\x0a\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09{{if\x20eq\x20.RootType\x20\"GOPATH\"}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09{{if\x20$.DirFlat}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09{{if\x20.HasPkg}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09{{html\x20.Path}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09{{else}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09{{html\x20.Name}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x09{{html\x20.Synopsis}}\x0a\x09\x09\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09
    \x0a\x09\x09\x09\x09\x20\x0a\x09\x09\x09\x20\x0a\x09\x09\x20\x0a\x09{{end}}\x0a\x0a\x09Other\x20packages\x0a\x09Sub-repositories\x0a\x09

    \x0a\x09These\x20packages\x20are\x20part\x20of\x20the\x20Go\x20Project\x20but\x20outside\x20the\x20main\x20Go\x20tree.\x0a\x09They\x20are\x20developed\x20under\x20looser\x20compatibility\x20requirements\x20than\x20the\x20Go\x20core.\x0a\x09Install\x20them\x20with\x20\"go\x20get\".\x0a\x09

    \x0a\x09
      \x0a\x09\x09
    • benchmarks\x20\xe2\x80\x94\x20benchmarks\x20to\x20measure\x20Go\x20as\x20it\x20is\x20developed.
    • \x0a\x09\x09
    • blog\x20\xe2\x80\x94\x20blog.golang.org's\x20implementation.
    • \x0a\x09\x09
    • build\x20\xe2\x80\x94\x20build.golang.org's\x20implementation.
    • \x0a\x09\x09
    • crypto\x20\xe2\x80\x94\x20additional\x20cryptography\x20packages.
    • \x0a\x09\x09
    • debug\x20\xe2\x80\x94\x20an\x20experimental\x20debugger\x20for\x20Go.
    • \x0a\x09\x09
    • image\x20\xe2\x80\x94\x20additional\x20imaging\x20packages.
    • \x0a\x09\x09
    • mobile\x20\xe2\x80\x94\x20experimental\x20support\x20for\x20Go\x20on\x20mobile\x20platforms.
    • \x0a\x09\x09
    • net\x20\xe2\x80\x94\x20additional\x20networking\x20packages.
    • \x0a\x09\x09
    • perf\x20\xe2\x80\x94\x20packages\x20and\x20tools\x20for\x20performance\x20measurement,\x20storage,\x20and\x20analysis.
    • \x0a\x09\x09
    • pkgsite\x20\xe2\x80\x94\x20home\x20of\x20the\x20pkg.go.dev\x20website.
    • \x0a\x09\x09
    • review\x20\xe2\x80\x94\x20a\x20tool\x20for\x20working\x20with\x20Gerrit\x20code\x20reviews.
    • \x0a\x09\x09
    • sync\x20\xe2\x80\x94\x20additional\x20concurrency\x20primitives.
    • \x0a\x09\x09
    • sys\x20\xe2\x80\x94\x20packages\x20for\x20making\x20system\x20calls.
    • \x0a\x09\x09
    • text\x20\xe2\x80\x94\x20packages\x20for\x20working\x20with\x20text.
    • \x0a\x09\x09
    • time\x20\xe2\x80\x94\x20additional\x20time\x20packages.
    • \x0a\x09\x09
    • tools\x20\xe2\x80\x94\x20godoc,\x20goimports,\x20gorename,\x20and\x20other\x20tools.
    • \x0a\x09\x09
    • tour\x20\xe2\x80\x94\x20tour.golang.org's\x20implementation.
    • \x0a\x09\x09
    • exp\x20\xe2\x80\x94\x20experimental\x20and\x20deprecated\x20packages\x20(handle\x20with\x20care;\x20may\x20change\x20without\x20warning).
    • \x0a\x09
    \x0a\x0a\x09Community\x0a\x09

    \x0a\x09These\x20services\x20can\x20help\x20you\x20find\x20Open\x20Source\x20packages\x20provided\x20by\x20the\x20community.\x0a\x09

    \x0a\x09
      \x0a\x09\x09
    • Pkg.go.dev\x20-\x20the\x20Go\x20package\x20discovery\x20site.
    • \x0a\x09\x09
    • Projects\x20at\x20the\x20Go\x20Wiki\x20-\x20a\x20curated\x20list\x20of\x20Go\x20projects.
    • \x0a\x09
    \x0a{{end}}\x0a", @@ -95,7 +95,7 @@ var Files = map[string]string{ "searchcode.html": "\x0a{{$query_url\x20:=\x20urlquery\x20.Query}}\x0a{{if\x20not\x20.Idents}}\x0a\x09{{with\x20.Pak}}\x0a\x09\x09Package\x20{{html\x20$.Query}}\x0a\x09\x09

    \x0a\x09\x09\x0a\x09\x09{{range\x20.}}\x0a\x09\x09\x09{{$pkg_html\x20:=\x20pkgLink\x20.Pak.Path\x20|\x20html}}\x0a\x09\x09\x09{{$pkg_html}}\x0a\x09\x09{{end}}\x0a\x09\x09\x0a\x09\x09

    \x0a\x09{{end}}\x0a{{end}}\x0a{{with\x20.Hit}}\x0a\x09{{with\x20.Decls}}\x0a\x09\x09Package-level\x20declarations\x0a\x09\x09{{range\x20.}}\x0a\x09\x09\x09{{$pkg_html\x20:=\x20pkgLink\x20.Pak.Path\x20|\x20html}}\x0a\x09\x09\x09package\x20{{html\x20.Pak.Name}}\x0a\x09\x09\x09{{range\x20.Files}}\x0a\x09\x09\x09\x09{{$file\x20:=\x20.File.Path}}\x0a\x09\x09\x09\x09{{range\x20.Groups}}\x0a\x09\x09\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09\x09\x09\x09{{$line\x20:=\x20infoLine\x20.}}\x0a\x09\x09\x09\x09\x09\x09{{$file}}:{{$line}}\x0a\x09\x09\x09\x09\x09\x09{{infoSnippet_html\x20.}}\x0a\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09{{end}}\x0a\x09{{with\x20.Others}}\x0a\x09\x09Local\x20declarations\x20and\x20uses\x0a\x09\x09{{range\x20.}}\x0a\x09\x09\x09{{$pkg_html\x20:=\x20pkgLink\x20.Pak.Path\x20|\x20html}}\x0a\x09\x09\x09package\x20{{html\x20.Pak.Name}}\x0a\x09\x09\x09{{range\x20.Files}}\x0a\x09\x09\x09\x09{{$file\x20:=\x20.File.Path}}\x0a\x09\x09\x09\x09{{$file}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09\x09{{range\x20.Groups}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09{{index\x20.\x200\x20|\x20infoKind_html}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09{{range\x20.}}\x0a\x09\x09\x09\x09\x09\x09{{$line\x20:=\x20infoLine\x20.}}\x0a\x09\x09\x09\x09\x09\x09{{$line}}\x0a\x09\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09\x09\x0a\x09\x09\x09\x09{{end}}\x0a\x09\x09\x09\x09\x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09{{end}}\x0a{{end}}\x0a", - "searchdoc.html": "\x0a{{range\x20$key,\x20$val\x20:=\x20.Idents}}\x0a\x09{{if\x20$val}}\x0a\x09\x09{{$key.Name}}\x0a\x09\x09{{range\x20$val}}\x0a\x09\x09\x09{{$pkg_html\x20:=\x20pkgLink\x20.Path\x20|\x20html}}\x0a\x09\x09\x09{{if\x20eq\x20\"Packages\"\x20$key.Name}}\x0a\x09\x09\x09\x09{{html\x20.Path}}\x0a\x09\x09\x09{{else}}\x0a\x09\x09\x09\x09{{$doc_html\x20:=\x20docLink\x20.Path\x20.Name|\x20html}}\x0a\x09\x09\x09\x09{{html\x20.Package}}.{{.Name}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{if\x20.Doc}}\x0a\x09\x09\x09\x09

    {{comment_html\x20.Doc}}

    \x0a\x09\x09\x09{{else}}\x0a\x09\x09\x09\x09

    No\x20documentation\x20available

    \x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09{{end}}\x0a{{end}}\x0a", + "searchdoc.html": "\x0a{{range\x20$key,\x20$val\x20:=\x20.Idents}}\x0a\x09{{if\x20$val}}\x0a\x09\x09{{$key.Name}}\x0a\x09\x09{{range\x20$val}}\x0a\x09\x09\x09{{$pkg_html\x20:=\x20pkgLink\x20.Path\x20|\x20html}}\x0a\x09\x09\x09{{if\x20eq\x20\"Packages\"\x20$key.Name}}\x0a\x09\x09\x09\x09{{html\x20.Path}}\x0a\x09\x09\x09{{else}}\x0a\x09\x09\x09\x09{{$doc_html\x20:=\x20docLink\x20.Path\x20.Name|\x20html}}\x0a\x09\x09\x09\x09{{html\x20.Package}}.{{.Name}}\x0a\x09\x09\x09{{end}}\x0a\x09\x09\x09{{if\x20.Doc}}\x0a\x09\x09\x09\x09

    {{comment_html\x20$\x20.Doc}}

    \x0a\x09\x09\x09{{else}}\x0a\x09\x09\x09\x09

    No\x20documentation\x20available

    \x0a\x09\x09\x09{{end}}\x0a\x09\x09{{end}}\x0a\x09{{end}}\x0a{{end}}\x0a", "searchtxt.html": "\x0a{{$query_url\x20:=\x20urlquery\x20.Query}}\x0a{{with\x20.Textual}}\x0a\x09{{if\x20$.Complete}}\x0a\x09\x09{{html\x20$.Found}}\x20textual\x20occurrences\x0a\x09{{else}}\x0a\x09\x09More\x20than\x20{{html\x20$.Found}}\x20textual\x20occurrences\x0a\x09\x09

    \x0a\x09\x09Not\x20all\x20files\x20or\x20lines\x20containing\x20\"{{html\x20$.Query}}\"\x20are\x20shown.\x0a\x09\x09

    \x0a\x09{{end}}\x0a\x09

    \x0a\x09\x0a\x09{{range\x20.}}\x0a\x09\x09{{$file\x20:=\x20.Filename}}\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09{{$file}}:\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09{{len\x20.Lines}}\x0a\x09\x09\x0a\x09\x09\x0a\x09\x09{{range\x20.Lines}}\x0a\x09\x09\x09{{html\x20.}}\x0a\x09\x09{{end}}\x0a\x09\x09{{if\x20not\x20$.Complete}}\x0a\x09\x09\x09...\x0a\x09\x09{{end}}\x0a\x09\x09\x0a\x09\x09\x0a\x09{{end}}\x0a\x09{{if\x20not\x20$.Complete}}\x0a\x09\x09...\x0a\x09{{end}}\x0a\x09\x0a\x09

    \x0a{{end}}\x0a", diff --git a/godoc/tohtml_go119.go b/godoc/tohtml_go119.go new file mode 100644 index 00000000000..6dbf7212b9a --- /dev/null +++ b/godoc/tohtml_go119.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package godoc + +import ( + "bytes" + "go/doc" +) + +func godocToHTML(buf *bytes.Buffer, pkg *doc.Package, comment string) { + buf.Write(pkg.HTML(comment)) +} diff --git a/godoc/tohtml_other.go b/godoc/tohtml_other.go new file mode 100644 index 00000000000..a1dcf2e195b --- /dev/null +++ b/godoc/tohtml_other.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.19 +// +build !go1.19 + +package godoc + +import ( + "bytes" + "go/doc" +) + +func godocToHTML(buf *bytes.Buffer, pkg *doc.Package, comment string) { + doc.ToHTML(buf, comment, nil) +} diff --git a/gopls/README.md b/gopls/README.md index 9afc2e48c1e..56d15921a70 100644 --- a/gopls/README.md +++ b/gopls/README.md @@ -5,56 +5,57 @@ `gopls` (pronounced "Go please") is the official Go [language server] developed by the Go team. It provides IDE features to any [LSP]-compatible editor. - + You should not need to interact with `gopls` directly--it will be automatically integrated into your editor. The specific features and settings vary slightly -by editor, so we recommend that you proceed to the [documentation for your -editor](#editors) below. +by editor, so we recommend that you proceed to the +[documentation for your editor](#editors) below. ## Editors To get started with `gopls`, install an LSP plugin in your editor of choice. -* [VSCode](https://github.com/golang/vscode-go/blob/master/README.md) +* [VS Code](https://github.com/golang/vscode-go/blob/master/README.md) * [Vim / Neovim](doc/vim.md) * [Emacs](doc/emacs.md) * [Atom](https://github.com/MordFustang21/ide-gopls) * [Sublime Text](doc/subl.md) * [Acme](https://github.com/fhs/acme-lsp) +* [Lapce](https://github.com/lapce-community/lapce-go) -If you use `gopls` with an editor that is not on this list, please let us know -by [filing an issue](#new-issue) or [modifying this documentation](doc/contributing.md). +If you use `gopls` with an editor that is not on this list, please send us a CL +[updating this documentation](doc/contributing.md). ## Installation For the most part, you should not need to install or update `gopls`. Your editor should handle that step for you. -If you do want to get the latest stable version of `gopls`, change to any -directory that is both outside of your `GOPATH` and outside of a module (a temp -directory is fine), and run: +If you do want to get the latest stable version of `gopls`, run the following +command: ```sh go install golang.org/x/tools/gopls@latest ``` -Learn more in the [advanced installation -instructions](doc/advanced.md#installing-unreleased-versions). +Learn more in the +[advanced installation instructions](doc/advanced.md#installing-unreleased-versions). + +Learn more about gopls releases in the [release policy](doc/releases.md). ## Setting up your workspace -`gopls` supports both Go module and GOPATH modes, but if you are working with -multiple modules or uncommon project layouts, you will need to specifically -configure your workspace. See the [Workspace document](doc/workspace.md) for -information on supported workspace layouts. +`gopls` supports both Go module, multi-module and GOPATH modes. See the +[workspace documentation](doc/workspace.md) for information on supported +workspace layouts. ## Configuration You can configure `gopls` to change your editor experience or view additional debugging information. Configuration options will be made available by your editor, so see your [editor's instructions](#editors) for specific details. A -full list of `gopls` settings can be found in the [Settings documentation](doc/settings.md). +full list of `gopls` settings can be found in the [settings documentation](doc/settings.md). ### Environment variables @@ -62,27 +63,36 @@ full list of `gopls` settings can be found in the [Settings documentation](doc/s variables you configure. Some editors, such as VS Code, allow users to selectively override the values of some environment variables. -## Troubleshooting +## Support Policy -If you are having issues with `gopls`, please follow the steps described in the -[troubleshooting guide](doc/troubleshooting.md). +Gopls is maintained by engineers on the +[Go tools team](https://github.com/orgs/golang/teams/tools-team/members), +who actively monitor the +[Go](https://github.com/golang/go/issues?q=is%3Aissue+is%3Aopen+label%3Agopls) +and +[VS Code Go](https://github.com/golang/vscode-go/issues) issue trackers. -## Supported Go versions and build systems +### Supported Go versions `gopls` follows the [Go Release Policy](https://golang.org/doc/devel/release.html#policy), meaning that it officially supports the last 2 major Go releases. Per -[issue #39146](golang.org/issues/39146), we attempt to maintain best-effort +[issue #39146](https://go.dev/issues/39146), we attempt to maintain best-effort support for the last 4 major Go releases, but this support extends only to not breaking the build and avoiding easily fixable regressions. -The following table shows the final gopls version that supports being built at -a given Go Version. Any more recent Go versions missing from this table can -still be built with the latest version of gopls. +In the context of this discussion, gopls "supports" a Go version if it supports +being built with that Go version as well as integrating with the `go` command +of that Go version. -| Go Version | Final gopls Version With Support | -| ----------- | -------------------------------- | +The following table shows the final gopls version that supports a given Go +version. Go releases more recent than any in the table can be used with any +version of gopls. + +| Go Version | Final gopls version with support (without warnings) | +| ----------- | --------------------------------------------------- | | Go 1.12 | [gopls@v0.7.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.7.5) | +| Go 1.15 | [gopls@v0.9.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.9.5) | Our extended support is enforced via [continuous integration with older Go versions](doc/contributing.md#ci). This legacy Go CI may not block releases: @@ -90,13 +100,22 @@ test failures may be skipped rather than fixed. Furthermore, if a regression in an older Go version causes irreconcilable CI failures, we may drop support for that Go version in CI if it is 3 or 4 Go versions old. -`gopls` currently only supports the `go` command, so if you are using a -different build system, `gopls` will not work well. Bazel is not officially -supported, but Bazel support is in development (see -[bazelbuild/rules_go#512](https://github.com/bazelbuild/rules_go/issues/512)). +### Supported build systems + +`gopls` currently only supports the `go` command, so if you are using +a different build system, `gopls` will not work well. Bazel is not officially +supported, but may be made to work with an appropriately configured +`go/packages` driver. See +[bazelbuild/rules_go#512](https://github.com/bazelbuild/rules_go/issues/512) +for more information. You can follow [these instructions](https://github.com/bazelbuild/rules_go/wiki/Editor-setup) to configure your `gopls` to work with Bazel. +### Troubleshooting + +If you are having issues with `gopls`, please follow the steps described in the +[troubleshooting guide](doc/troubleshooting.md). + ## Additional information * [Features](doc/features.md) @@ -110,4 +129,3 @@ to configure your `gopls` to work with Bazel. [language server]: https://langserver.org [LSP]: https://microsoft.github.io/language-server-protocol/ -[Gophers Slack]: https://gophers.slack.com/ diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go index 167bdbd1b9f..8bb54186bab 100644 --- a/gopls/api-diff/api_diff.go +++ b/gopls/api-diff/api_diff.go @@ -13,262 +13,77 @@ import ( "encoding/json" "flag" "fmt" - "io" - "io/ioutil" "log" "os" "os/exec" - "path/filepath" - "strings" - "golang.org/x/tools/internal/gocommand" - difflib "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/source" + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/lsp/source" ) -var ( - previousVersionFlag = flag.String("prev", "", "version to compare against") - versionFlag = flag.String("version", "", "version being tagged, or current version if omitted") -) +const usage = `api-diff [] + +Compare the API of two gopls versions. If the second argument is provided, it +will be used as the new version to compare against. Otherwise, compare against +the current API. +` func main() { flag.Parse() - apiDiff, err := diffAPI(*versionFlag, *previousVersionFlag) + if flag.NArg() < 1 || flag.NArg() > 2 { + fmt.Fprint(os.Stderr, usage) + os.Exit(2) + } + + oldVer := flag.Arg(0) + newVer := "" + if flag.NArg() == 2 { + newVer = flag.Arg(1) + } + + apiDiff, err := diffAPI(oldVer, newVer) if err != nil { log.Fatal(err) } - fmt.Printf(` -%s -`, apiDiff) -} - -type JSON interface { - String() string - Write(io.Writer) + fmt.Println("\n" + apiDiff) } -func diffAPI(version, prev string) (string, error) { +func diffAPI(oldVer, newVer string) (string, error) { ctx := context.Background() - previousApi, err := loadAPI(ctx, prev) + previousAPI, err := loadAPI(ctx, oldVer) if err != nil { - return "", fmt.Errorf("load previous API: %v", err) + return "", fmt.Errorf("loading %s: %v", oldVer, err) } - var currentApi *source.APIJSON - if version == "" { - currentApi = source.GeneratedAPIJSON + var currentAPI *source.APIJSON + if newVer == "" { + currentAPI = source.GeneratedAPIJSON } else { var err error - currentApi, err = loadAPI(ctx, version) + currentAPI, err = loadAPI(ctx, newVer) if err != nil { - return "", fmt.Errorf("load current API: %v", err) - } - } - - b := &strings.Builder{} - if err := diff(b, previousApi.Commands, currentApi.Commands, "command", func(c *source.CommandJSON) string { - return c.Command - }, diffCommands); err != nil { - return "", fmt.Errorf("diff commands: %v", err) - } - if diff(b, previousApi.Analyzers, currentApi.Analyzers, "analyzer", func(a *source.AnalyzerJSON) string { - return a.Name - }, diffAnalyzers); err != nil { - return "", fmt.Errorf("diff analyzers: %v", err) - } - if err := diff(b, previousApi.Lenses, currentApi.Lenses, "code lens", func(l *source.LensJSON) string { - return l.Lens - }, diffLenses); err != nil { - return "", fmt.Errorf("diff lenses: %v", err) - } - for key, prev := range previousApi.Options { - current, ok := currentApi.Options[key] - if !ok { - panic(fmt.Sprintf("unexpected option key: %s", key)) - } - if err := diff(b, prev, current, "option", func(o *source.OptionJSON) string { - return o.Name - }, diffOptions); err != nil { - return "", fmt.Errorf("diff options (%s): %v", key, err) + return "", fmt.Errorf("loading %s: %v", newVer, err) } } - return b.String(), nil + return cmp.Diff(previousAPI, currentAPI), nil } -func diff[T JSON](b *strings.Builder, previous, new []T, kind string, uniqueKey func(T) string, diffFunc func(*strings.Builder, T, T)) error { - prevJSON := collect(previous, uniqueKey) - newJSON := collect(new, uniqueKey) - for k := range newJSON { - delete(prevJSON, k) - } - for _, deleted := range prevJSON { - b.WriteString(fmt.Sprintf("%s %s was deleted.\n", kind, deleted)) - } - for _, prev := range previous { - delete(newJSON, uniqueKey(prev)) - } - if len(newJSON) > 0 { - b.WriteString("The following commands were added:\n") - for _, n := range newJSON { - n.Write(b) - b.WriteByte('\n') - } - } - previousMap := collect(previous, uniqueKey) - for _, current := range new { - prev, ok := previousMap[uniqueKey(current)] - if !ok { - continue - } - c, p := bytes.NewBuffer(nil), bytes.NewBuffer(nil) - prev.Write(p) - current.Write(c) - if diff, err := diffStr(p.String(), c.String()); err == nil && diff != "" { - diffFunc(b, prev, current) - b.WriteString("\n--\n") - } - } - return nil -} - -func collect[T JSON](args []T, uniqueKey func(T) string) map[string]T { - m := map[string]T{} - for _, arg := range args { - m[uniqueKey(arg)] = arg - } - return m -} - -var goCmdRunner = gocommand.Runner{} - func loadAPI(ctx context.Context, version string) (*source.APIJSON, error) { - tmpGopath, err := ioutil.TempDir("", "gopath*") - if err != nil { - return nil, fmt.Errorf("temp dir: %v", err) - } - defer os.RemoveAll(tmpGopath) + ver := fmt.Sprintf("golang.org/x/tools/gopls@%s", version) + cmd := exec.Command("go", "run", ver, "api-json") - exampleDir := fmt.Sprintf("%s/src/example.com", tmpGopath) - if err := os.MkdirAll(exampleDir, 0776); err != nil { - return nil, fmt.Errorf("mkdir: %v", err) - } + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stderr - if stdout, err := goCmdRunner.Run(ctx, gocommand.Invocation{ - Verb: "mod", - Args: []string{"init", "example.com"}, - WorkingDir: exampleDir, - Env: append(os.Environ(), fmt.Sprintf("GOPATH=%s", tmpGopath)), - }); err != nil { - return nil, fmt.Errorf("go mod init failed: %v (stdout: %v)", err, stdout) - } - if stdout, err := goCmdRunner.Run(ctx, gocommand.Invocation{ - Verb: "install", - Args: []string{fmt.Sprintf("golang.org/x/tools/gopls@%s", version)}, - WorkingDir: exampleDir, - Env: append(os.Environ(), fmt.Sprintf("GOPATH=%s", tmpGopath)), - }); err != nil { - return nil, fmt.Errorf("go install failed: %v (stdout: %v)", err, stdout.String()) - } - cmd := exec.Cmd{ - Path: filepath.Join(tmpGopath, "bin", "gopls"), - Args: []string{"gopls", "api-json"}, - Dir: tmpGopath, - } - out, err := cmd.Output() - if err != nil { - return nil, fmt.Errorf("output: %v", err) + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("go run failed: %v; stderr:\n%s", err, stderr) } apiJson := &source.APIJSON{} - if err := json.Unmarshal(out, apiJson); err != nil { + if err := json.Unmarshal(stdout.Bytes(), apiJson); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } return apiJson, nil } - -func diffCommands(b *strings.Builder, prev, current *source.CommandJSON) { - if prev.Title != current.Title { - b.WriteString(fmt.Sprintf("Title changed from %q to %q\n", prev.Title, current.Title)) - } - if prev.Doc != current.Doc { - b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", prev.Doc, current.Doc)) - } - if prev.ArgDoc != current.ArgDoc { - b.WriteString("Arguments changed from " + formatBlock(prev.ArgDoc) + " to " + formatBlock(current.ArgDoc)) - } - if prev.ResultDoc != current.ResultDoc { - b.WriteString("Results changed from " + formatBlock(prev.ResultDoc) + " to " + formatBlock(current.ResultDoc)) - } -} - -func diffAnalyzers(b *strings.Builder, previous, current *source.AnalyzerJSON) { - b.WriteString(fmt.Sprintf("Changes to analyzer %s:\n\n", current.Name)) - if previous.Doc != current.Doc { - b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", previous.Doc, current.Doc)) - } - if previous.Default != current.Default { - b.WriteString(fmt.Sprintf("Default changed from %v to %v\n", previous.Default, current.Default)) - } -} - -func diffLenses(b *strings.Builder, previous, current *source.LensJSON) { - b.WriteString(fmt.Sprintf("Changes to code lens %s:\n\n", current.Title)) - if previous.Title != current.Title { - b.WriteString(fmt.Sprintf("Title changed from %q to %q\n", previous.Title, current.Title)) - } - if previous.Doc != current.Doc { - b.WriteString(fmt.Sprintf("Documentation changed from %q to %q\n", previous.Doc, current.Doc)) - } -} - -func diffOptions(b *strings.Builder, previous, current *source.OptionJSON) { - b.WriteString(fmt.Sprintf("Changes to option %s:\n\n", current.Name)) - if previous.Doc != current.Doc { - diff, err := diffStr(previous.Doc, current.Doc) - if err != nil { - panic(err) - } - b.WriteString(fmt.Sprintf("Documentation changed:\n%s\n", diff)) - } - if previous.Default != current.Default { - b.WriteString(fmt.Sprintf("Default changed from %q to %q\n", previous.Default, current.Default)) - } - if previous.Hierarchy != current.Hierarchy { - b.WriteString(fmt.Sprintf("Categorization changed from %q to %q\n", previous.Hierarchy, current.Hierarchy)) - } - if previous.Status != current.Status { - b.WriteString(fmt.Sprintf("Status changed from %q to %q\n", previous.Status, current.Status)) - } - if previous.Type != current.Type { - b.WriteString(fmt.Sprintf("Type changed from %q to %q\n", previous.Type, current.Type)) - } - // TODO(rstambler): Handle possibility of same number but different keys/values. - if len(previous.EnumKeys.Keys) != len(current.EnumKeys.Keys) { - b.WriteString(fmt.Sprintf("Enum keys changed from\n%s\n to \n%s\n", previous.EnumKeys, current.EnumKeys)) - } - if len(previous.EnumValues) != len(current.EnumValues) { - b.WriteString(fmt.Sprintf("Enum values changed from\n%s\n to \n%s\n", previous.EnumValues, current.EnumValues)) - } -} - -func formatBlock(str string) string { - if str == "" { - return `""` - } - return "\n```\n" + str + "\n```\n" -} - -func diffStr(before, after string) (string, error) { - // Add newlines to avoid newline messages in diff. - if before == after { - return "", nil - } - before += "\n" - after += "\n" - d, err := myers.ComputeEdits("", before, after) - if err != nil { - return "", err - } - return fmt.Sprintf("%q", difflib.ToUnified("previous", "current", before, d)), err -} diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index f5c83d5771d..176c32f1ba4 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -131,7 +131,7 @@ of the second argument is not a pointer to a type implementing error. find structs that would use less memory if their fields were sorted This analyzer find structs that can be rearranged to use less memory, and provides -a suggested edit with the optimal order. +a suggested edit with the most compact order. Note that there are two different diagnostics reported. One checks struct size, and the other reports "pointer bytes" used. Pointer bytes is how many bytes of the @@ -150,6 +150,11 @@ has 24 pointer bytes because it has to scan further through the *uint32. has 8 because it can stop immediately after the string pointer. +Be aware that the most compact order is not always the most efficient. +In rare cases it may cause two variables each updated by its own goroutine +to occupy the same CPU cache line, inducing a form of memory contention +known as "false sharing" that slows down both goroutines. + **Disabled by default. Enable it by setting `"analyses": {"fieldalignment": true}`.** @@ -213,11 +218,16 @@ inferred from function arguments, or from other type arguments: check references to loop variables from within nested functions -This analyzer checks for references to loop variables from within a -function literal inside the loop body. It checks only instances where -the function literal is called in a defer or go statement that is the -last statement in the loop body, as otherwise we would need whole -program analysis. +This analyzer checks for references to loop variables from within a function +literal inside the loop body. It checks for patterns where access to a loop +variable is known to escape the current loop iteration: + 1. a call to go or defer at the end of the loop body + 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body + 3. a call testing.T.Run where the subtest body invokes t.Parallel() + +In the case of (1) and (2), the analyzer only considers references in the last +statement of the loop body as it is not deep enough to understand the effects +of subsequent statements which might render the reference benign. For example: @@ -490,6 +500,17 @@ identifiers. Please see the documentation for package testing in golang.org/pkg/testing for the conventions that are enforced for Tests, Benchmarks, and Examples. +**Enabled by default.** + +## **timeformat** + +check for calls of (time.Time).Format or time.Parse with 2006-02-01 + +The timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm) +format. Internationally, "yyyy-dd-mm" does not occur in common calendar date +standards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended. + + **Enabled by default.** ## **unmarshal** @@ -652,6 +673,15 @@ func <>(inferred parameters) { **Enabled by default.** +## **unusedvariable** + +check for unused variables + +The unusedvariable analyzer suggests fixes for unused variables errors. + + +**Disabled by default. Enable it by setting `"analyses": {"unusedvariable": true}`.** + ## **fillstruct** note incomplete struct initializations diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index f868a48936e..c942be48fda 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -247,6 +247,20 @@ Args: } ``` +### **Reset go.mod diagnostics** +Identifier: `gopls.reset_go_mod_diagnostics` + +Reset diagnostics in the go.mod file of a module. + +Args: + +``` +{ + // The file URI. + "URI": string, +} +``` + ### **Run test(s)** Identifier: `gopls.run_tests` @@ -274,33 +288,13 @@ Args: ``` { - // Dir is the directory from which vulncheck will run from. - "Dir": string, + // Any document in the directory from which govulncheck will run. + "URI": string, // Package pattern. E.g. "", ".", "./...". "Pattern": string, } ``` -Result: - -``` -{ - "Vuln": []{ - "ID": string, - "Details": string, - "Aliases": []string, - "Symbol": string, - "PkgPath": string, - "ModPath": string, - "URL": string, - "CurrentVersion": string, - "FixedVersion": string, - "CallStacks": [][]golang.org/x/tools/internal/lsp/command.StackEntry, - "CallStackSummaries": []string, - }, -} -``` - ### **Start the gopls debug server** Identifier: `gopls.start_debugging` diff --git a/gopls/doc/contributing.md b/gopls/doc/contributing.md index 99e45292296..367280f53e3 100644 --- a/gopls/doc/contributing.md +++ b/gopls/doc/contributing.md @@ -18,8 +18,8 @@ claiming it. ## Getting started -Most of the `gopls` logic is actually in the `golang.org/x/tools/internal/lsp` -directory, so you are most likely to develop in the golang.org/x/tools module. +Most of the `gopls` logic is in the `golang.org/x/tools/gopls/internal/lsp` +directory. ## Build diff --git a/gopls/doc/design/integrating.md b/gopls/doc/design/integrating.md index 845f9eb007f..ba2cc07aa71 100644 --- a/gopls/doc/design/integrating.md +++ b/gopls/doc/design/integrating.md @@ -20,7 +20,7 @@ Many LSP requests pass position or range information. This is described in the [ This means that integrators will need to calculate UTF-16 based column offsets. -[`golang.org/x/tools/internal/span`] has the code to do this in go. +[`golang.org/x/tools/gopls/internal/span`] has the code to do this in go. [#31080] tracks making `span` and other useful packages non-internal. ## Edits @@ -61,9 +61,9 @@ For instance, files that are needed to do correct type checking are modified by Monitoring files inside gopls directly has a lot of awkward problems, but the [LSP specification] has methods that allow gopls to request that the client notify it of file system changes, specifically [`workspace/didChangeWatchedFiles`]. This is currently being added to gopls by a community member, and tracked in [#31553] -[InitializeResult]: https://pkg.go.dev/golang.org/x/tools/internal/lsp/protocol#InitializeResult -[ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/internal/lsp/protocol#ServerCapabilities -[`golang.org/x/tools/internal/span`]: https://pkg.go.dev/golang.org/x/tools/internal/span#NewPoint +[InitializeResult]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#InitializeResult +[ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#ServerCapabilities +[`golang.org/x/tools/gopls/internal/span`]: https://pkg.go.dev/golang.org/x/tools/internal/span#NewPoint [LSP specification]: https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/ [lsp-response]: https://github.com/Microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md#response-message diff --git a/gopls/doc/generate.go b/gopls/doc/generate.go index e63653de6bc..9ab2d01a3f2 100644 --- a/gopls/doc/generate.go +++ b/gopls/doc/generate.go @@ -20,6 +20,7 @@ import ( "io" "io/ioutil" "os" + "os/exec" "path/filepath" "reflect" "regexp" @@ -32,47 +33,70 @@ import ( "github.com/jba/printsrc" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/command/commandmeta" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/command/commandmeta" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/source" ) func main() { - if _, err := doMain("..", true); err != nil { + if _, err := doMain(true); err != nil { fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err) os.Exit(1) } } -func doMain(baseDir string, write bool) (bool, error) { +func doMain(write bool) (bool, error) { api, err := loadAPI() if err != nil { return false, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "internal/lsp/source/api_json.go"), api, write, rewriteAPI); !ok || err != nil { + sourceDir, err := pkgDir("golang.org/x/tools/gopls/internal/lsp/source") + if err != nil { + return false, err + } + + if ok, err := rewriteFile(filepath.Join(sourceDir, "api_json.go"), api, write, rewriteAPI); !ok || err != nil { + return ok, err + } + + goplsDir, err := pkgDir("golang.org/x/tools/gopls") + if err != nil { + return false, err + } + + if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "settings.md"), api, write, rewriteSettings); !ok || err != nil { return ok, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/settings.md"), api, write, rewriteSettings); !ok || err != nil { + if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "commands.md"), api, write, rewriteCommands); !ok || err != nil { return ok, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/commands.md"), api, write, rewriteCommands); !ok || err != nil { + if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "analyzers.md"), api, write, rewriteAnalyzers); !ok || err != nil { return ok, err } - if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/analyzers.md"), api, write, rewriteAnalyzers); !ok || err != nil { + if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "inlayHints.md"), api, write, rewriteInlayHints); !ok || err != nil { return ok, err } return true, nil } +// pkgDir returns the directory corresponding to the import path pkgPath. +func pkgDir(pkgPath string) (string, error) { + out, err := exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath).Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + func loadAPI() (*source.APIJSON, error) { pkgs, err := packages.Load( &packages.Config{ Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps, }, - "golang.org/x/tools/internal/lsp/source", + "golang.org/x/tools/gopls/internal/lsp/source", ) if err != nil { return nil, err @@ -102,6 +126,7 @@ func loadAPI() (*source.APIJSON, error) { } { api.Analyzers = append(api.Analyzers, loadAnalyzers(m)...) } + api.Hints = loadHints(source.AllInlayHints) for _, category := range []reflect.Value{ reflect.ValueOf(defaults.UserOptions), } { @@ -146,6 +171,14 @@ func loadAPI() (*source.APIJSON, error) { Default: def, }) } + case "hints": + for _, a := range api.Hints { + opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, source.EnumKey{ + Name: fmt.Sprintf("%q", a.Name), + Doc: a.Doc, + Default: strconv.FormatBool(a.Default), + }) + } } } } @@ -488,6 +521,23 @@ func loadAnalyzers(m map[string]*source.Analyzer) []*source.AnalyzerJSON { return json } +func loadHints(m map[string]*source.Hint) []*source.HintJSON { + var sorted []string + for _, h := range m { + sorted = append(sorted, h.Name) + } + sort.Strings(sorted) + var json []*source.HintJSON + for _, name := range sorted { + h := m[name] + json = append(json, &source.HintJSON{ + Name: h.Name, + Doc: h.Doc, + }) + } + return json +} + func lowerFirst(x string) string { if x == "" { return x @@ -537,7 +587,7 @@ func rewriteFile(file string, api *source.APIJSON, write bool, rewrite func([]by func rewriteAPI(_ []byte, api *source.APIJSON) ([]byte, error) { var buf bytes.Buffer fmt.Fprintf(&buf, "// Code generated by \"golang.org/x/tools/gopls/doc/generate\"; DO NOT EDIT.\n\npackage source\n\nvar GeneratedAPIJSON = ") - if err := printsrc.NewPrinter("golang.org/x/tools/internal/lsp/source").Fprint(&buf, api); err != nil { + if err := printsrc.NewPrinter("golang.org/x/tools/gopls/internal/lsp/source").Fprint(&buf, api); err != nil { return nil, err } return format.Source(buf.Bytes()) @@ -571,7 +621,7 @@ func rewriteSettings(doc []byte, api *source.APIJSON) ([]byte, error) { writeTitle(section, h.final, level) for _, opt := range h.options { header := strMultiply("#", level+1) - section.Write([]byte(fmt.Sprintf("%s ", header))) + fmt.Fprintf(section, "%s ", header) opt.Write(section) } } @@ -699,6 +749,21 @@ func rewriteAnalyzers(doc []byte, api *source.APIJSON) ([]byte, error) { return replaceSection(doc, "Analyzers", section.Bytes()) } +func rewriteInlayHints(doc []byte, api *source.APIJSON) ([]byte, error) { + section := bytes.NewBuffer(nil) + for _, hint := range api.Hints { + fmt.Fprintf(section, "## **%v**\n\n", hint.Name) + fmt.Fprintf(section, "%s\n\n", hint.Doc) + switch hint.Default { + case true: + fmt.Fprintf(section, "**Enabled by default.**\n\n") + case false: + fmt.Fprintf(section, "**Disabled by default. Enable it by setting `\"hints\": {\"%s\": true}`.**\n\n", hint.Name) + } + } + return replaceSection(doc, "Hints", section.Bytes()) +} + func replaceSection(doc []byte, sectionName string, replacement []byte) ([]byte, error) { re := regexp.MustCompile(fmt.Sprintf(`(?s)\n(.*?)`, sectionName, sectionName)) idx := re.FindSubmatchIndex(doc) diff --git a/gopls/doc/generate_test.go b/gopls/doc/generate_test.go index 137a646cd8d..d33594d6159 100644 --- a/gopls/doc/generate_test.go +++ b/gopls/doc/generate_test.go @@ -16,7 +16,7 @@ import ( func TestGenerated(t *testing.T) { testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. - ok, err := doMain("../..", false) + ok, err := doMain(false) if err != nil { t.Fatal(err) } diff --git a/gopls/doc/inlayHints.md b/gopls/doc/inlayHints.md new file mode 100644 index 00000000000..2ae9a2828af --- /dev/null +++ b/gopls/doc/inlayHints.md @@ -0,0 +1,80 @@ +# Hints + +This document describes the inlay hints that `gopls` uses inside the editor. + + +## **assignVariableTypes** + +Enable/disable inlay hints for variable types in assign statements: +```go + i/* int*/, j/* int*/ := 0, len(r)-1 +``` + +**Disabled by default. Enable it by setting `"hints": {"assignVariableTypes": true}`.** + +## **compositeLiteralFields** + +Enable/disable inlay hints for composite literal field names: +```go + {/*in: */"Hello, world", /*want: */"dlrow ,olleH"} +``` + +**Disabled by default. Enable it by setting `"hints": {"compositeLiteralFields": true}`.** + +## **compositeLiteralTypes** + +Enable/disable inlay hints for composite literal types: +```go + for _, c := range []struct { + in, want string + }{ + /*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"}, + } +``` + +**Disabled by default. Enable it by setting `"hints": {"compositeLiteralTypes": true}`.** + +## **constantValues** + +Enable/disable inlay hints for constant values: +```go + const ( + KindNone Kind = iota/* = 0*/ + KindPrint/* = 1*/ + KindPrintf/* = 2*/ + KindErrorf/* = 3*/ + ) +``` + +**Disabled by default. Enable it by setting `"hints": {"constantValues": true}`.** + +## **functionTypeParameters** + +Enable/disable inlay hints for implicit type parameters on generic functions: +```go + myFoo/*[int, string]*/(1, "hello") +``` + +**Disabled by default. Enable it by setting `"hints": {"functionTypeParameters": true}`.** + +## **parameterNames** + +Enable/disable inlay hints for parameter names: +```go + parseInt(/* str: */ "123", /* radix: */ 8) +``` + +**Disabled by default. Enable it by setting `"hints": {"parameterNames": true}`.** + +## **rangeVariableTypes** + +Enable/disable inlay hints for variable types in range statements: +```go + for k/* int*/, v/* string*/ := range []string{} { + fmt.Println(k, v) + } +``` + +**Disabled by default. Enable it by setting `"hints": {"rangeVariableTypes": true}`.** + + diff --git a/gopls/doc/releases.md b/gopls/doc/releases.md new file mode 100644 index 00000000000..befb92c3966 --- /dev/null +++ b/gopls/doc/releases.md @@ -0,0 +1,25 @@ +# Gopls release policy + +Gopls releases follow [semver](http://semver.org), with major changes and new +features introduced only in new minor versions (i.e. versions of the form +`v*.N.0` for some N). Subsequent patch releases contain only cherry-picked +fixes or superficial updates. + +In order to align with the +[Go release timeline](https://github.com/golang/go/wiki/Go-Release-Cycle#timeline), +we aim to release a new minor version of Gopls approximately every three +months, with patch releases approximately every month, according to the +following table: + +| Month | Version(s) | +| ---- | ------- | +| Jan | `v*..0` | +| Jan-Mar | `v*..*` | +| Apr | `v*..0` | +| Apr-Jun | `v*..*` | +| Jul | `v*..0` | +| Jul-Sep | `v*..*` | +| Oct | `v*..0` | +| Oct-Dec | `v*..*` | + +For more background on this policy, see https://go.dev/issue/55267. diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 092a3c7cfaf..5595976363f 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -1,6 +1,6 @@ # Settings - + This document describes the global settings for `gopls` inside the editor. The settings block will be called `"gopls"` and contains a collection of @@ -35,6 +35,7 @@ still be able to independently override specific experimental features. * [Completion](#completion) * [Diagnostic](#diagnostic) * [Documentation](#documentation) + * [Inlayhint](#inlayhint) * [Navigation](#navigation) ### Build @@ -62,15 +63,19 @@ relative to the workspace folder. They are evaluated in order, and the last filter that applies to a path controls whether it is included. The path prefix can be empty, so an initial `-` excludes everything. +DirectoryFilters also supports the `**` operator to match 0 or more directories. + Examples: -Exclude node_modules: `-node_modules` +Exclude node_modules at current depth: `-node_modules` + +Exclude node_modules at any depth: `-**/node_modules` Include only project_a: `-` (exclude everything), `+project_a` Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules` -Default: `["-node_modules"]`. +Default: `["-**/node_modules"]`. #### **templateExtensions** *[]string* @@ -118,6 +123,9 @@ Default: `true`. experimentalWorkspaceModule opts a user into the experimental support for multi-module workspaces. +Deprecated: this feature is deprecated and will be removed in a future +version of gopls (https://go.dev/issue/55331). + Default: `false`. #### **experimentalPackageCacheKey** *bool* @@ -159,11 +167,36 @@ Default: `false`. experimentalUseInvalidMetadata enables gopls to fall back on outdated package metadata to provide editor features if the go command fails to -load packages for some reason (like an invalid go.mod file). This will -eventually be the default behavior, and this setting will be removed. +load packages for some reason (like an invalid go.mod file). + +Deprecated: this setting is deprecated and will be removed in a future +version of gopls (https://go.dev/issue/55333). Default: `false`. +#### **standaloneTags** *[]string* + +standaloneTags specifies a set of build constraints that identify +individual Go source files that make up the entire main package of an +executable. + +A common example of standalone main files is the convention of using the +directive `//go:build ignore` to denote files that are not intended to be +included in any package, for example because they are invoked directly by +the developer using `go run`. + +Gopls considers a file to be a standalone main file if and only if it has +package name "main" and has a build directive of the exact form +"//go:build tag" or "// +build tag", where tag is among the list of tags +configured by this setting. Notably, if the build constraint is more +complicated than a simple tag (such as the composite constraint +`//go:build tag && go1.18`), the file is not considered to be a standalone +main file. + +This setting is only supported when gopls is built with Go 1.16 or later. + +Default: `["ignore"]`. + ### Formatting #### **local** *string* @@ -214,6 +247,22 @@ semantic tokens to the client. Default: `false`. +#### **noSemanticString** *bool* + +**This setting is experimental and may be deleted.** + +noSemanticString turns off the sending of the semantic token 'string' + +Default: `false`. + +#### **noSemanticNumber** *bool* + +**This setting is experimental and may be deleted.** + +noSemanticNumber turns off the sending of the semantic token 'number' + +Default: `false`. + #### Completion ##### **usePlaceholders** *bool* @@ -265,8 +314,8 @@ Default: `true`. analyses specify analyses that the user would like to enable or disable. A map of the names of analysis passes that should be enabled/disabled. -A full list of analyzers that gopls uses can be found -[here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). +A full list of analyzers that gopls uses can be found in +[analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). Example Usage: @@ -286,6 +335,8 @@ Default: `{}`. **This setting is experimental and may be deleted.** staticcheck enables additional analyses from staticcheck.io. +These analyses are documented on +[Staticcheck's website](https://staticcheck.io/docs/checks/). Default: `false`. @@ -330,6 +381,9 @@ file system notifications. This option must be set to a valid duration string, for example `"100ms"`. +Deprecated: this setting is deprecated and will be removed in a future +version of gopls (https://go.dev/issue/55332) + Default: `"0s"`. #### Documentation @@ -362,6 +416,9 @@ It might be one of: If company chooses to use its own `godoc.org`, its address can be used as well. +Modules matching the GOPRIVATE environment variable will not have +documentation links in hover. + Default: `"pkg.go.dev"`. ##### **linksInHover** *bool* @@ -370,6 +427,18 @@ linksInHover toggles the presence of links to documentation in hover. Default: `true`. +#### Inlayhint + +##### **hints** *map[string]bool* + +**This setting is experimental and may be deleted.** + +hints specify inlay hints that users want to see. A full list of hints +that gopls uses can be found in +[inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). + +Default: `{}`. + #### Navigation ##### **importShortcut** *enum* @@ -439,6 +508,17 @@ Default: `false`. +#### **newDiff** *string* + +newDiff enables the new diff implementation. If this is "both", for now both +diffs will be run and statistics will be generated in a file in $TMPDIR. This +is a risky setting; help in trying it is appreciated. If it is "old" the old +implementation is used, and if it is "new", just the new implementation is +used. This setting will eventually be deleted, once gopls has fully migrated to +the new diff algorithm. + +Default: 'both'. + ## Code Lenses These are the code lenses that `gopls` currently supports. They can be enabled @@ -461,6 +541,11 @@ Runs `go generate` for a given directory. Identifier: `regenerate_cgo` Regenerates cgo definitions. +### **Run vulncheck (experimental)** + +Identifier: `run_vulncheck_exp` + +Run vulnerability check (`govulncheck`). ### **Run test(s) (legacy)** Identifier: `test` diff --git a/gopls/doc/vim.md b/gopls/doc/vim.md index d9b33ac34dc..af54a7e088e 100644 --- a/gopls/doc/vim.md +++ b/gopls/doc/vim.md @@ -175,23 +175,22 @@ a helper function in Lua: lua < ../ diff --git a/gopls/go.sum b/gopls/go.sum index 5873afa1968..9810051995b 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -1,23 +1,25 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= -github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= -github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/safehtml v0.0.2 h1:ZOt2VXg4x24bW0m2jtzAOkhoXV0iM8vNKc0paByCZqM= github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= +github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= +github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/jba/printsrc v0.2.2 h1:9OHK51UT+/iMAEBlQIIXW04qvKyF3/vvLuwW/hL8tDU= github.com/jba/printsrc v0.2.2/go.mod h1:1xULjw59sL0dPdWpDoVU06TIEO/Wnfv6AHRpiElTwYM= github.com/jba/templatecheck v0.6.0 h1:SwM8C4hlK/YNLsdcXStfnHWE2HKkuTVwy5FKQHt5ro8= @@ -33,62 +35,71 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= +golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326 h1:fl8k2zg28yA23264d82M4dp+YlJ3ngDcpuB1bewkQi4= +golang.org/x/exp/typeparams v0.0.0-20221031165847-c99f073a8326/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211213223007-03aa0b5f6827/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be h1:jokAF1mfylAi1iTQx7C44B7vyXUcSEMw8eDv0PzNu8s= -golang.org/x/vuln v0.0.0-20220503210553-a5481fb0c8be/go.mod h1:twca1SxmF6/i2wHY/mj1vLIkkHdp+nil/yA32ZOP4kg= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9 h1:KaYZQUtEEaV8aVADIHAuYBTjo77aUcCvC7KTGKM3J1I= +golang.org/x/vuln v0.0.0-20221010193109-563322be2ea9/go.mod h1:F12iebNzxRMpJsm4W7ape+r/KdnXiSy3VC94WsyCG68= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= -honnef.co/go/tools v0.3.0 h1:2LdYUZ7CIxnYgskbUZfY7FPggmqnh6shBqfWa8Tn3XU= -honnef.co/go/tools v0.3.0/go.mod h1:vlRD9XErLMGT+mDuofSr0mMMquscM/1nQqtRSsh6m70= -mvdan.cc/gofumpt v0.3.0 h1:kTojdZo9AcEYbQYhGuLf/zszYthRdhDNDUi2JKTxas4= -mvdan.cc/gofumpt v0.3.0/go.mod h1:0+VyGZWleeIj5oostkOex+nDBA0eyavuDnDusAJ8ylo= +honnef.co/go/tools v0.3.3 h1:oDx7VAwstgpYpb3wv0oxiZlxY+foCpRAwY7Vk6XpAgA= +honnef.co/go/tools v0.3.3/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= +mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= +mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5 h1:Jh3LAeMt1eGpxomyu3jVkmVZWW2MxZ1qIIV2TZ/nRio= mvdan.cc/unparam v0.0.0-20211214103731-d0ef000c54e5/go.mod h1:b8RRCBm0eeiWR8cfN88xeq2G5SG3VKGO+5UPWi5FSOY= mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc= diff --git a/gopls/internal/coverage/coverage.go b/gopls/internal/coverage/coverage.go index 7bb3640bdbd..9a7d219945e 100644 --- a/gopls/internal/coverage/coverage.go +++ b/gopls/internal/coverage/coverage.go @@ -12,9 +12,13 @@ // -o controls where the coverage file is written, defaulting to /tmp/cover.out // -i coverage-file will generate the report from an existing coverage file // -v controls verbosity (0: only report coverage, 1: report as each directory is finished, -// 2: report on each test, 3: more details, 4: too much) +// +// 2: report on each test, 3: more details, 4: too much) +// // -t tests only tests packages in the given comma-separated list of directories in gopls. -// The names should start with ., as in ./internal/regtest/bench +// +// The names should start with ., as in ./internal/regtest/bench +// // -run tests. If set, -run tests is passed on to the go test command. // // Despite gopls' use of goroutines, the counts are almost deterministic. @@ -60,7 +64,7 @@ func main() { tests = realTestName(tests) // report coverage for packages under internal/lsp - parg := "golang.org/x/tools/internal/lsp/..." + parg := "golang.org/x/tools/gopls/internal/lsp/..." accum := []string{} seen := make(map[string]bool) @@ -184,7 +188,12 @@ func maybePrint(m result) { if *verbose > 3 { fmt.Printf("%s %s %q %.3f\n", m.Action, m.Test, m.Output, m.Elapsed) } + case "pause", "cont": + if *verbose > 2 { + fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) + } default: + fmt.Printf("%#v\n", m) log.Fatalf("unknown action %s\n", m.Action) } } @@ -224,7 +233,7 @@ func checkCwd() { if err != nil { log.Fatal(err) } - // we expect to be a the root of golang.org/x/tools + // we expect to be at the root of golang.org/x/tools cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "golang.org/x/tools") buf, err := cmd.Output() buf = bytes.Trim(buf, "\n \t") // remove \n at end @@ -239,10 +248,6 @@ func checkCwd() { if err != nil { log.Fatalf("expected a gopls directory, %v", err) } - _, err = os.Stat("internal/lsp") - if err != nil { - log.Fatalf("expected to see internal/lsp, %v", err) - } } func listDirs(dir string) []string { diff --git a/gopls/internal/govulncheck/README.md b/gopls/internal/govulncheck/README.md index d8339c506f6..bc10d8a2ec1 100644 --- a/gopls/internal/govulncheck/README.md +++ b/gopls/internal/govulncheck/README.md @@ -15,3 +15,5 @@ The `copy.sh` does the copying, after removing all .go files here. To use it: 2. cd to this directory. 3. Run `copy.sh`. + +4. Re-add build tags for go1.18 \ No newline at end of file diff --git a/gopls/internal/govulncheck/cache.go b/gopls/internal/govulncheck/cache.go index 404c3567320..2fa6a05d727 100644 --- a/gopls/internal/govulncheck/cache.go +++ b/gopls/internal/govulncheck/cache.go @@ -11,7 +11,6 @@ package govulncheck import ( "encoding/json" "go/build" - "io/ioutil" "os" "path/filepath" "sync" @@ -66,7 +65,7 @@ func (c *FSCache) ReadIndex(dbName string) (client.DBIndex, time.Time, error) { c.mu.Lock() defer c.mu.Unlock() - b, err := ioutil.ReadFile(filepath.Join(c.rootDir, dbName, "index.json")) + b, err := os.ReadFile(filepath.Join(c.rootDir, dbName, "index.json")) if err != nil { if os.IsNotExist(err) { return nil, time.Time{}, nil @@ -95,7 +94,7 @@ func (c *FSCache) WriteIndex(dbName string, index client.DBIndex, retrieved time if err != nil { return err } - if err := ioutil.WriteFile(filepath.Join(path, "index.json"), j, 0666); err != nil { + if err := os.WriteFile(filepath.Join(path, "index.json"), j, 0666); err != nil { return err } return nil @@ -105,7 +104,11 @@ func (c *FSCache) ReadEntries(dbName string, p string) ([]*osv.Entry, error) { c.mu.Lock() defer c.mu.Unlock() - b, err := ioutil.ReadFile(filepath.Join(c.rootDir, dbName, p, "vulns.json")) + ep, err := client.EscapeModulePath(p) + if err != nil { + return nil, err + } + b, err := os.ReadFile(filepath.Join(c.rootDir, dbName, ep, "vulns.json")) if err != nil { if os.IsNotExist(err) { return nil, nil @@ -123,7 +126,11 @@ func (c *FSCache) WriteEntries(dbName string, p string, entries []*osv.Entry) er c.mu.Lock() defer c.mu.Unlock() - path := filepath.Join(c.rootDir, dbName, p) + ep, err := client.EscapeModulePath(p) + if err != nil { + return err + } + path := filepath.Join(c.rootDir, dbName, ep) if err := os.MkdirAll(path, 0777); err != nil { return err } @@ -131,7 +138,7 @@ func (c *FSCache) WriteEntries(dbName string, p string, entries []*osv.Entry) er if err != nil { return err } - if err := ioutil.WriteFile(filepath.Join(path, "vulns.json"), j, 0666); err != nil { + if err := os.WriteFile(filepath.Join(path, "vulns.json"), j, 0666); err != nil { return err } return nil diff --git a/gopls/internal/govulncheck/cache_test.go b/gopls/internal/govulncheck/cache_test.go index 5a25c781020..57e87659046 100644 --- a/gopls/internal/govulncheck/cache_test.go +++ b/gopls/internal/govulncheck/cache_test.go @@ -93,7 +93,7 @@ func TestConcurrency(t *testing.T) { i := i g.Go(func() error { id := i % 5 - p := fmt.Sprintf("package%d", id) + p := fmt.Sprintf("example.com/package%d", id) entries, err := cache.ReadEntries(dbName, p) if err != nil { @@ -115,7 +115,7 @@ func TestConcurrency(t *testing.T) { // sanity checking for i := 0; i < 5; i++ { id := fmt.Sprint(i) - p := fmt.Sprintf("package%s", id) + p := fmt.Sprintf("example.com/package%s", id) es, err := cache.ReadEntries(dbName, p) if err != nil { diff --git a/gopls/internal/govulncheck/copy.sh b/gopls/internal/govulncheck/copy.sh index 24ed45bfe5a..398cde2f466 100755 --- a/gopls/internal/govulncheck/copy.sh +++ b/gopls/internal/govulncheck/copy.sh @@ -11,3 +11,22 @@ set -o pipefail rm -f *.go cp ../../../../vuln/cmd/govulncheck/internal/govulncheck/*.go . + +sed -i '' 's/\"golang.org\/x\/vuln\/internal\/semver\"/\"golang.org\/x\/tools\/gopls\/internal\/govulncheck\/semver\"/g' *.go +sed -i '' -e '4 i\ +' -e '4 i\ +//go:build go1.18' -e '4 i\ +// +build go1.18' *.go + +# Copy golang.org/x/vuln/internal/semver that +# golang.org/x/vuln/cmd/govulncheck/internal/govulncheck +# depends on. + +mkdir -p semver +cd semver +rm -f *.go +cp ../../../../../vuln/internal/semver/*.go . +sed -i '' -e '4 i\ +' -e '4 i\ +//go:build go1.18' -e '4 i\ +// +build go1.18' *.go diff --git a/gopls/internal/govulncheck/filepath.go b/gopls/internal/govulncheck/filepath.go new file mode 100644 index 00000000000..cef78f5a47c --- /dev/null +++ b/gopls/internal/govulncheck/filepath.go @@ -0,0 +1,38 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package govulncheck + +import ( + "path/filepath" + "strings" +) + +// AbsRelShorter takes path and returns its path relative +// to the current directory, if shorter. Returns path +// when path is an empty string or upon any error. +func AbsRelShorter(path string) string { + if path == "" { + return "" + } + + c, err := filepath.Abs(".") + if err != nil { + return path + } + r, err := filepath.Rel(c, path) + if err != nil { + return path + } + + rSegments := strings.Split(r, string(filepath.Separator)) + pathSegments := strings.Split(path, string(filepath.Separator)) + if len(rSegments) < len(pathSegments) { + return r + } + return path +} diff --git a/gopls/internal/govulncheck/filepath_test.go b/gopls/internal/govulncheck/filepath_test.go new file mode 100644 index 00000000000..06ef40a1239 --- /dev/null +++ b/gopls/internal/govulncheck/filepath_test.go @@ -0,0 +1,42 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package govulncheck + +import ( + "os" + "path/filepath" + "testing" +) + +func TestAbsRelShorter(t *testing.T) { + thisFile := "filepath_test.go" + thisFileAbs, _ := filepath.Abs(thisFile) + + tf, err := os.CreateTemp("", "filepath_test.gp") + if err != nil { + t.Errorf("could not create temporary filepath_test.go file: %v", err) + } + tempFile := tf.Name() + tempFileAbs, _ := filepath.Abs(tempFile) + + for _, test := range []struct { + l string + want string + }{ + {thisFile, "filepath_test.go"}, + {thisFileAbs, "filepath_test.go"}, + // Relative path to temp file from "." is longer as + // it needs to go back the length of the absolute + // path and then in addition go to os.TempDir. + {tempFile, tempFileAbs}, + } { + if got := AbsRelShorter(test.l); got != test.want { + t.Errorf("want %s; got %s", test.want, got) + } + } +} diff --git a/gopls/internal/govulncheck/semver/semver.go b/gopls/internal/govulncheck/semver/semver.go new file mode 100644 index 00000000000..8b1cfe55ea2 --- /dev/null +++ b/gopls/internal/govulncheck/semver/semver.go @@ -0,0 +1,84 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +// Package semver provides shared utilities for manipulating +// Go semantic versions. +package semver + +import ( + "regexp" + "strings" +) + +// addSemverPrefix adds a 'v' prefix to s if it isn't already prefixed +// with 'v' or 'go'. This allows us to easily test go-style SEMVER +// strings against normal SEMVER strings. +func addSemverPrefix(s string) string { + if !strings.HasPrefix(s, "v") && !strings.HasPrefix(s, "go") { + return "v" + s + } + return s +} + +// removeSemverPrefix removes the 'v' or 'go' prefixes from go-style +// SEMVER strings, for usage in the public vulnerability format. +func removeSemverPrefix(s string) string { + s = strings.TrimPrefix(s, "v") + s = strings.TrimPrefix(s, "go") + return s +} + +// CanonicalizeSemverPrefix turns a SEMVER string into the canonical +// representation using the 'v' prefix, as used by the OSV format. +// Input may be a bare SEMVER ("1.2.3"), Go prefixed SEMVER ("go1.2.3"), +// or already canonical SEMVER ("v1.2.3"). +func CanonicalizeSemverPrefix(s string) string { + return addSemverPrefix(removeSemverPrefix(s)) +} + +var ( + // Regexp for matching go tags. The groups are: + // 1 the major.minor version + // 2 the patch version, or empty if none + // 3 the entire prerelease, if present + // 4 the prerelease type ("beta" or "rc") + // 5 the prerelease number + tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc|-pre)(\d+))?$`) +) + +// This is a modified copy of pkgsite/internal/stdlib:VersionForTag. +func GoTagToSemver(tag string) string { + if tag == "" { + return "" + } + + tag = strings.Fields(tag)[0] + // Special cases for go1. + if tag == "go1" { + return "v1.0.0" + } + if tag == "go1.0" { + return "" + } + m := tagRegexp.FindStringSubmatch(tag) + if m == nil { + return "" + } + version := "v" + m[1] + if m[2] != "" { + version += m[2] + } else { + version += ".0" + } + if m[3] != "" { + if !strings.HasPrefix(m[4], "-") { + version += "-" + } + version += m[4] + "." + m[5] + } + return version +} diff --git a/gopls/internal/govulncheck/semver/semver_test.go b/gopls/internal/govulncheck/semver/semver_test.go new file mode 100644 index 00000000000..56b6ea89999 --- /dev/null +++ b/gopls/internal/govulncheck/semver/semver_test.go @@ -0,0 +1,43 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package semver + +import ( + "testing" +) + +func TestCanonicalize(t *testing.T) { + for _, test := range []struct { + v string + want string + }{ + {"v1.2.3", "v1.2.3"}, + {"1.2.3", "v1.2.3"}, + {"go1.2.3", "v1.2.3"}, + } { + got := CanonicalizeSemverPrefix(test.v) + if got != test.want { + t.Errorf("want %s; got %s", test.want, got) + } + } +} + +func TestGoTagToSemver(t *testing.T) { + for _, test := range []struct { + v string + want string + }{ + {"go1.19", "v1.19.0"}, + {"go1.20-pre4", "v1.20.0-pre.4"}, + } { + got := GoTagToSemver(test.v) + if got != test.want { + t.Errorf("want %s; got %s", test.want, got) + } + } +} diff --git a/gopls/internal/govulncheck/source.go b/gopls/internal/govulncheck/source.go index 752a8313091..d3f519d86ed 100644 --- a/gopls/internal/govulncheck/source.go +++ b/gopls/internal/govulncheck/source.go @@ -8,13 +8,11 @@ package govulncheck import ( - "context" "fmt" "sort" "strings" "golang.org/x/tools/go/packages" - "golang.org/x/vuln/client" "golang.org/x/vuln/vulncheck" ) @@ -27,7 +25,7 @@ func (e *PackageError) Error() string { var b strings.Builder fmt.Fprintln(&b, "Packages contain errors:") for _, e := range e.Errors { - fmt.Println(&b, e) + fmt.Fprintln(&b, e) } return b.String() } @@ -57,34 +55,23 @@ func LoadPackages(cfg *packages.Config, patterns ...string) ([]*vulncheck.Packag return vpkgs, err } -// Source calls vulncheck.Source on the Go source in pkgs. It returns the result -// with Vulns trimmed to those that are actually called. -func Source(ctx context.Context, pkgs []*vulncheck.Package, c client.Client) (*vulncheck.Result, error) { - r, err := vulncheck.Source(ctx, pkgs, &vulncheck.Config{Client: c}) - if err != nil { - return nil, err - } - // Keep only the vulns that are called. - var vulns []*vulncheck.Vuln - for _, v := range r.Vulns { - if v.CallSink != 0 { - vulns = append(vulns, v) - } - } - r.Vulns = vulns - return r, nil -} - // CallInfo is information about calls to vulnerable functions. type CallInfo struct { - CallStacks map[*vulncheck.Vuln][]vulncheck.CallStack // all call stacks - VulnGroups [][]*vulncheck.Vuln // vulns grouped by ID and package - ModuleVersions map[string]string // map from module paths to versions - TopPackages map[string]bool // top-level packages + // CallStacks contains all call stacks to vulnerable functions. + CallStacks map[*vulncheck.Vuln][]vulncheck.CallStack + + // VulnGroups contains vulnerabilities grouped by ID and package. + VulnGroups [][]*vulncheck.Vuln + + // ModuleVersions is a map of module paths to versions. + ModuleVersions map[string]string + + // TopPackages contains the top-level packages in the call info. + TopPackages map[string]bool } // GetCallInfo computes call stacks and related information from a vulncheck.Result. -// I also makes a set of top-level packages from pkgs. +// It also makes a set of top-level packages from pkgs. func GetCallInfo(r *vulncheck.Result, pkgs []*vulncheck.Package) *CallInfo { pset := map[string]bool{} for _, p := range pkgs { diff --git a/gopls/internal/govulncheck/summary.go b/gopls/internal/govulncheck/summary.go new file mode 100644 index 00000000000..e389b89a4ad --- /dev/null +++ b/gopls/internal/govulncheck/summary.go @@ -0,0 +1,46 @@ +// 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 govulncheck + +import "golang.org/x/vuln/osv" + +// TODO(hyangah): Find a better package for these types +// unless golang.org/x/vuln/exp/govulncheck starts to export these. + +// Summary is the govulncheck result. +type Summary struct { + // Vulnerabilities affecting the analysis target binary or source code. + Affecting []Vuln + // Vulnerabilities that may be imported but the vulnerable symbols are + // not called. For binary analysis, this will be always empty. + NonAffecting []Vuln +} + +// Vuln represents a vulnerability relevant to a (module, package). +type Vuln struct { + OSV *osv.Entry + PkgPath string // Package path. + ModPath string // Module path. + FoundIn string // @ if we know when it was introduced. Empty otherwise. + FixedIn string // @ if fix is available. Empty otherwise. + // Trace contains a call stack for each affecting symbol. + // For vulnerabilities found from binary analysis, and vulnerabilities + // that are reported as Unaffecting ones, this will be always empty. + Trace []Trace +} + +// Trace represents a sample trace for a vulnerable symbol. +type Trace struct { + Symbol string // Name of the detected vulnerable function or method. + Desc string // One-line description of the callstack. + Stack []StackEntry // Call stack. + Seen int // Number of similar call stacks. +} + +// StackEntry represents a call stack entry. +type StackEntry struct { + FuncName string // Function name is the function name, adjusted to remove pointer annotation. + CallSite string // Position of the call/reference site. It is one of the formats token.Pos.String() returns or empty if unknown. +} diff --git a/gopls/internal/govulncheck/util.go b/gopls/internal/govulncheck/util.go index baa2d961329..fc63d5678ad 100644 --- a/gopls/internal/govulncheck/util.go +++ b/gopls/internal/govulncheck/util.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/mod/semver" + isem "golang.org/x/tools/gopls/internal/govulncheck/semver" "golang.org/x/vuln/osv" "golang.org/x/vuln/vulncheck" ) @@ -24,7 +25,8 @@ func LatestFixed(as []osv.Affected) string { for _, r := range a.Ranges { if r.Type == osv.TypeSemver { for _, e := range r.Events { - if e.Fixed != "" && (v == "" || semver.Compare(e.Fixed, v) > 0) { + if e.Fixed != "" && (v == "" || + semver.Compare(isem.CanonicalizeSemverPrefix(e.Fixed), isem.CanonicalizeSemverPrefix(v)) > 0) { v = e.Fixed } } @@ -60,12 +62,16 @@ func SummarizeCallStack(cs vulncheck.CallStack, topPkgs map[string]bool, vulnPkg } iVuln += iTop + 1 // adjust for slice in call to highest. topName := FuncName(cs[iTop].Function) + topPos := AbsRelShorter(FuncPos(cs[iTop].Call)) + if topPos != "" { + topPos += ": " + } vulnName := FuncName(cs[iVuln].Function) if iVuln == iTop+1 { - return fmt.Sprintf("%s calls %s", topName, vulnName) + return fmt.Sprintf("%s%s calls %s", topPos, topName, vulnName) } - return fmt.Sprintf("%s calls %s, which eventually calls %s", - topName, FuncName(cs[iTop+1].Function), vulnName) + return fmt.Sprintf("%s%s calls %s, which eventually calls %s", + topPos, topName, FuncName(cs[iTop+1].Function), vulnName) } // highest returns the highest (one with the smallest index) entry in the call @@ -107,3 +113,11 @@ func PkgPath(fn *vulncheck.FuncNode) string { func FuncName(fn *vulncheck.FuncNode) string { return strings.TrimPrefix(fn.String(), "*") } + +// FuncPos returns the function position from call. +func FuncPos(call *vulncheck.CallSite) string { + if call != nil && call.Pos != nil { + return call.Pos.String() + } + return "" +} diff --git a/gopls/internal/hooks/analysis.go b/gopls/internal/hooks/analysis.go deleted file mode 100644 index 51048991d5a..00000000000 --- a/gopls/internal/hooks/analysis.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.17 -// +build go1.17 - -package hooks - -import ( - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "honnef.co/go/tools/analysis/lint" - "honnef.co/go/tools/quickfix" - "honnef.co/go/tools/simple" - "honnef.co/go/tools/staticcheck" - "honnef.co/go/tools/stylecheck" -) - -func updateAnalyzers(options *source.Options) { - options.StaticcheckSupported = true - - mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { - switch severity { - case lint.SeverityError: - return protocol.SeverityError - case lint.SeverityDeprecated: - // TODO(dh): in LSP, deprecated is a tag, not a severity. - // We'll want to support this once we enable SA5011. - return protocol.SeverityWarning - case lint.SeverityWarning: - return protocol.SeverityWarning - case lint.SeverityInfo: - return protocol.SeverityInformation - case lint.SeverityHint: - return protocol.SeverityHint - default: - return protocol.SeverityWarning - } - } - add := func(analyzers []*lint.Analyzer, skip map[string]struct{}) { - for _, a := range analyzers { - if _, ok := skip[a.Analyzer.Name]; ok { - continue - } - - enabled := !a.Doc.NonDefault - options.AddStaticcheckAnalyzer(a.Analyzer, enabled, mapSeverity(a.Doc.Severity)) - } - } - - add(simple.Analyzers, nil) - add(staticcheck.Analyzers, map[string]struct{}{ - // This check conflicts with the vet printf check (golang/go#34494). - "SA5009": {}, - // This check relies on facts from dependencies, which - // we don't currently compute. - "SA5011": {}, - }) - add(stylecheck.Analyzers, nil) - add(quickfix.Analyzers, nil) -} diff --git a/gopls/internal/hooks/analysis_116.go b/gopls/internal/hooks/analysis_116.go new file mode 100644 index 00000000000..dd429dea898 --- /dev/null +++ b/gopls/internal/hooks/analysis_116.go @@ -0,0 +1,14 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.17 +// +build !go1.17 + +package hooks + +import "golang.org/x/tools/gopls/internal/lsp/source" + +func updateAnalyzers(options *source.Options) { + options.StaticcheckSupported = false +} diff --git a/gopls/internal/hooks/analysis_117.go b/gopls/internal/hooks/analysis_117.go index 02f9170ab63..27ab9a699f9 100644 --- a/gopls/internal/hooks/analysis_117.go +++ b/gopls/internal/hooks/analysis_117.go @@ -1,14 +1,62 @@ -// Copyright 2021 The Go Authors. All rights reserved. +// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !go1.17 -// +build !go1.17 +//go:build go1.17 +// +build go1.17 package hooks -import "golang.org/x/tools/internal/lsp/source" +import ( + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "honnef.co/go/tools/analysis/lint" + "honnef.co/go/tools/quickfix" + "honnef.co/go/tools/simple" + "honnef.co/go/tools/staticcheck" + "honnef.co/go/tools/stylecheck" +) func updateAnalyzers(options *source.Options) { - options.StaticcheckSupported = false + options.StaticcheckSupported = true + + mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { + switch severity { + case lint.SeverityError: + return protocol.SeverityError + case lint.SeverityDeprecated: + // TODO(dh): in LSP, deprecated is a tag, not a severity. + // We'll want to support this once we enable SA5011. + return protocol.SeverityWarning + case lint.SeverityWarning: + return protocol.SeverityWarning + case lint.SeverityInfo: + return protocol.SeverityInformation + case lint.SeverityHint: + return protocol.SeverityHint + default: + return protocol.SeverityWarning + } + } + add := func(analyzers []*lint.Analyzer, skip map[string]struct{}) { + for _, a := range analyzers { + if _, ok := skip[a.Analyzer.Name]; ok { + continue + } + + enabled := !a.Doc.NonDefault + options.AddStaticcheckAnalyzer(a.Analyzer, enabled, mapSeverity(a.Doc.Severity)) + } + } + + add(simple.Analyzers, nil) + add(staticcheck.Analyzers, map[string]struct{}{ + // This check conflicts with the vet printf check (golang/go#34494). + "SA5009": {}, + // This check relies on facts from dependencies, which + // we don't currently compute. + "SA5011": {}, + }) + add(stylecheck.Analyzers, nil) + add(quickfix.Analyzers, nil) } diff --git a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go index a307ba77fd6..a0383b87675 100644 --- a/gopls/internal/hooks/diff.go +++ b/gopls/internal/hooks/diff.go @@ -5,37 +5,166 @@ package hooks import ( + "encoding/json" "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "sync" + "time" "github.com/sergi/go-diff/diffmatchpatch" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/diff" ) -func ComputeEdits(uri span.URI, before, after string) (edits []diff.TextEdit, err error) { +// structure for saving information about diffs +// while the new code is being rolled out +type diffstat struct { + Before, After int + Oldedits, Newedits int + Oldtime, Newtime time.Duration + Stack string + Msg string `json:",omitempty"` // for errors + Ignored int `json:",omitempty"` // numbr of skipped records with 0 edits +} + +var ( + ignoredMu sync.Mutex + ignored int // counter of diff requests on equal strings + + diffStatsOnce sync.Once + diffStats *os.File // never closed +) + +// save writes a JSON record of statistics about diff requests to a temporary file. +func (s *diffstat) save() { + diffStatsOnce.Do(func() { + f, err := ioutil.TempFile("", "gopls-diff-stats-*") + if err != nil { + log.Printf("can't create diff stats temp file: %v", err) // e.g. disk full + return + } + diffStats = f + }) + if diffStats == nil { + return + } + + // diff is frequently called with equal strings, + // so we count repeated instances but only print every 15th. + ignoredMu.Lock() + if s.Oldedits == 0 && s.Newedits == 0 { + ignored++ + if ignored < 15 { + ignoredMu.Unlock() + return + } + } + s.Ignored = ignored + ignored = 0 + ignoredMu.Unlock() + + // Record the name of the file in which diff was called. + // There aren't many calls, so only the base name is needed. + if _, file, line, ok := runtime.Caller(2); ok { + s.Stack = fmt.Sprintf("%s:%d", filepath.Base(file), line) + } + x, err := json.Marshal(s) + if err != nil { + log.Fatalf("internal error marshalling JSON: %v", err) + } + fmt.Fprintf(diffStats, "%s\n", x) +} + +// disaster is called when the diff algorithm panics or produces a +// diff that cannot be applied. It saves the broken input in a +// new temporary file and logs the file name, which is returned. +func disaster(before, after string) string { + // We use the pid to salt the name, not os.TempFile, + // so that each process creates at most one file. + // One is sufficient for a bug report. + filename := fmt.Sprintf("%s/gopls-diff-bug-%x", os.TempDir(), os.Getpid()) + + // We use NUL as a separator: it should never appear in Go source. + data := before + "\x00" + after + + if err := ioutil.WriteFile(filename, []byte(data), 0600); err != nil { + log.Printf("failed to write diff bug report: %v", err) + return "" + } + + // TODO(adonovan): is there a better way to surface this? + log.Printf("Bug detected in diff algorithm! Please send file %s to the maintainers of gopls if you are comfortable sharing its contents.", filename) + + return filename +} + +// BothDiffs edits calls both the new and old diffs, checks that the new diffs +// change before into after, and attempts to preserve some statistics. +func BothDiffs(before, after string) (edits []diff.Edit) { + // The new diff code contains a lot of internal checks that panic when they + // fail. This code catches the panics, or other failures, tries to save + // the failing example (and it would ask the user to send it back to us, and + // changes options.newDiff to 'old', if only we could figure out how.) + stat := diffstat{Before: len(before), After: len(after)} + now := time.Now() + oldedits := ComputeEdits(before, after) + stat.Oldedits = len(oldedits) + stat.Oldtime = time.Since(now) + defer func() { + if r := recover(); r != nil { + disaster(before, after) + edits = oldedits + } + }() + now = time.Now() + newedits := diff.Strings(before, after) + stat.Newedits = len(newedits) + stat.Newtime = time.Now().Sub(now) + got, err := diff.Apply(before, newedits) + if err != nil || got != after { + stat.Msg += "FAIL" + disaster(before, after) + stat.save() + return oldedits + } + stat.save() + return newedits +} + +// ComputeEdits computes a diff using the github.com/sergi/go-diff implementation. +func ComputeEdits(before, after string) (edits []diff.Edit) { // The go-diff library has an unresolved panic (see golang/go#278774). // TODO(rstambler): Remove the recover once the issue has been fixed // upstream. defer func() { if r := recover(); r != nil { - edits = nil - err = fmt.Errorf("unable to compute edits for %s: %s", uri.Filename(), r) + bug.Reportf("unable to compute edits: %s", r) + // Report one big edit for the whole file. + edits = []diff.Edit{{ + Start: 0, + End: len(before), + New: after, + }} } }() diffs := diffmatchpatch.New().DiffMain(before, after, true) - edits = make([]diff.TextEdit, 0, len(diffs)) + edits = make([]diff.Edit, 0, len(diffs)) offset := 0 for _, d := range diffs { - start := span.NewPoint(0, 0, offset) + start := offset switch d.Type { case diffmatchpatch.DiffDelete: offset += len(d.Text) - edits = append(edits, diff.TextEdit{Span: span.New(uri, start, span.NewPoint(0, 0, offset))}) + edits = append(edits, diff.Edit{Start: start, End: offset}) case diffmatchpatch.DiffEqual: offset += len(d.Text) case diffmatchpatch.DiffInsert: - edits = append(edits, diff.TextEdit{Span: span.New(uri, start, span.Point{}), NewText: d.Text}) + edits = append(edits, diff.Edit{Start: start, End: start, New: d.Text}) } } - return edits, nil + return edits } diff --git a/gopls/internal/hooks/diff_test.go b/gopls/internal/hooks/diff_test.go index d979be78dbe..a46bf3b2d28 100644 --- a/gopls/internal/hooks/diff_test.go +++ b/gopls/internal/hooks/diff_test.go @@ -2,15 +2,32 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package hooks_test +package hooks import ( + "io/ioutil" + "os" "testing" - "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/diff/difftest" + "golang.org/x/tools/internal/diff/difftest" ) func TestDiff(t *testing.T) { - difftest.DiffTest(t, hooks.ComputeEdits) + difftest.DiffTest(t, ComputeEdits) +} + +func TestDisaster(t *testing.T) { + a := "This is a string,(\u0995) just for basic\nfunctionality" + b := "This is another string, (\u0996) to see if disaster will store stuff correctly" + fname := disaster(a, b) + buf, err := ioutil.ReadFile(fname) + if err != nil { + t.Fatal(err) + } + if string(buf) != a+"\x00"+b { + t.Error("failed to record original strings") + } + if err := os.Remove(fname); err != nil { + t.Error(err) + } } diff --git a/gopls/internal/hooks/gen-licenses.sh b/gopls/internal/hooks/gen-licenses.sh index 7d6bab79f54..c35c91260d4 100755 --- a/gopls/internal/hooks/gen-licenses.sh +++ b/gopls/internal/hooks/gen-licenses.sh @@ -27,7 +27,7 @@ mods=$(go list -deps -f '{{with .Module}}{{.Path}}{{end}}' golang.org/x/tools/go for mod in $mods; do # Find the license file, either LICENSE or COPYING, and add it to the result. dir=$(go list -m -f {{.Dir}} $mod) - license=$(ls -1 $dir | egrep -i '^(LICENSE|COPYING)$') + license=$(ls -1 $dir | grep -E -i '^(LICENSE|COPYING)$') echo "-- $mod $license --" >> $tempfile echo >> $tempfile sed 's/^-- / &/' $dir/$license >> $tempfile diff --git a/gopls/internal/hooks/gofumpt_117.go b/gopls/internal/hooks/gofumpt_117.go new file mode 100644 index 00000000000..71886357704 --- /dev/null +++ b/gopls/internal/hooks/gofumpt_117.go @@ -0,0 +1,13 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.18 +// +build !go1.18 + +package hooks + +import "golang.org/x/tools/gopls/internal/lsp/source" + +func updateGofumpt(options *source.Options) { +} diff --git a/gopls/internal/hooks/gofumpt_118.go b/gopls/internal/hooks/gofumpt_118.go new file mode 100644 index 00000000000..4eb523261dc --- /dev/null +++ b/gopls/internal/hooks/gofumpt_118.go @@ -0,0 +1,24 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package hooks + +import ( + "context" + + "golang.org/x/tools/gopls/internal/lsp/source" + "mvdan.cc/gofumpt/format" +) + +func updateGofumpt(options *source.Options) { + options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { + return format.Source(src, format.Options{ + LangVersion: langVersion, + ModulePath: modulePath, + }) + } +} diff --git a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go index 023aefeab98..5624a5eb386 100644 --- a/gopls/internal/hooks/hooks.go +++ b/gopls/internal/hooks/hooks.go @@ -8,27 +8,24 @@ package hooks // import "golang.org/x/tools/gopls/internal/hooks" import ( - "context" - - "golang.org/x/tools/gopls/internal/vulncheck" - "golang.org/x/tools/internal/lsp/source" - "mvdan.cc/gofumpt/format" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" "mvdan.cc/xurls/v2" ) func Options(options *source.Options) { options.LicensesText = licensesText if options.GoDiff { - options.ComputeEdits = ComputeEdits + switch options.NewDiff { + case "old": + options.ComputeEdits = ComputeEdits + case "new": + options.ComputeEdits = diff.Strings + default: + options.ComputeEdits = BothDiffs + } } options.URLRegexp = xurls.Relaxed() - options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { - return format.Source(src, format.Options{ - LangVersion: langVersion, - ModulePath: modulePath, - }) - } updateAnalyzers(options) - - options.Govulncheck = vulncheck.Govulncheck + updateGofumpt(options) } diff --git a/gopls/internal/hooks/licenses_test.go b/gopls/internal/hooks/licenses_test.go index 3b61d348d95..b10d7e2b36c 100644 --- a/gopls/internal/hooks/licenses_test.go +++ b/gopls/internal/hooks/licenses_test.go @@ -15,9 +15,9 @@ import ( ) func TestLicenses(t *testing.T) { - // License text differs for older Go versions because staticcheck isn't - // supported for those versions. - testenv.NeedsGo1Point(t, 17) + // License text differs for older Go versions because staticcheck or gofumpt + // isn't supported for those versions. + testenv.NeedsGo1Point(t, 18) if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { t.Skip("generating licenses only works on Unixes") diff --git a/internal/lsp/README.md b/gopls/internal/lsp/README.md similarity index 100% rename from internal/lsp/README.md rename to gopls/internal/lsp/README.md diff --git a/internal/lsp/analysis/embeddirective/embeddirective.go b/gopls/internal/lsp/analysis/embeddirective/embeddirective.go similarity index 100% rename from internal/lsp/analysis/embeddirective/embeddirective.go rename to gopls/internal/lsp/analysis/embeddirective/embeddirective.go diff --git a/internal/lsp/analysis/embeddirective/embeddirective_test.go b/gopls/internal/lsp/analysis/embeddirective/embeddirective_test.go similarity index 100% rename from internal/lsp/analysis/embeddirective/embeddirective_test.go rename to gopls/internal/lsp/analysis/embeddirective/embeddirective_test.go diff --git a/internal/lsp/analysis/embeddirective/testdata/src/a/a.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/embeddirective/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/embeddirective/testdata/src/a/a.go diff --git a/internal/lsp/analysis/embeddirective/testdata/src/a/b.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/b.go similarity index 100% rename from internal/lsp/analysis/embeddirective/testdata/src/a/b.go rename to gopls/internal/lsp/analysis/embeddirective/testdata/src/a/b.go diff --git a/internal/lsp/analysis/embeddirective/testdata/src/a/embedText b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/embedText similarity index 100% rename from internal/lsp/analysis/embeddirective/testdata/src/a/embedText rename to gopls/internal/lsp/analysis/embeddirective/testdata/src/a/embedText diff --git a/internal/lsp/analysis/fillreturns/fillreturns.go b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go similarity index 87% rename from internal/lsp/analysis/fillreturns/fillreturns.go rename to gopls/internal/lsp/analysis/fillreturns/fillreturns.go index 72fe65d79ca..4415ddd66ec 100644 --- a/internal/lsp/analysis/fillreturns/fillreturns.go +++ b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go @@ -19,6 +19,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/typeparams" ) @@ -70,6 +71,8 @@ outer: } // Get the end position of the error. + // (This heuristic assumes that the buffer is formatted, + // at least up to the end position of the error.) var buf bytes.Buffer if err := format.Node(&buf, pass.Fset, file); err != nil { continue @@ -112,7 +115,7 @@ outer: break } } - if enclosingFunc == nil { + if enclosingFunc == nil || enclosingFunc.Results == nil { continue } @@ -155,19 +158,23 @@ outer: fixed := make([]ast.Expr, len(enclosingFunc.Results.List)) // For each value in the return function declaration, find the leftmost element - // in the return statement that has the desired type. If no such element exits, + // in the return statement that has the desired type. If no such element exists, // fill in the missing value with the appropriate "zero" value. + // Beware that type information may be incomplete. var retTyps []types.Type for _, ret := range enclosingFunc.Results.List { - retTyps = append(retTyps, info.TypeOf(ret.Type)) + retTyp := info.TypeOf(ret.Type) + if retTyp == nil { + return nil, nil + } + retTyps = append(retTyps, retTyp) } - matches := - analysisinternal.FindMatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) + matches := analysisinternal.MatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) for i, retTyp := range retTyps { var match ast.Expr var idx int for j, val := range remaining { - if !matchingTypes(info.TypeOf(val), retTyp) { + if t := info.TypeOf(val); t == nil || !matchingTypes(t, retTyp) { continue } if !analysisinternal.IsZeroValue(val) { @@ -184,21 +191,19 @@ outer: fixed[i] = match remaining = append(remaining[:idx], remaining[idx+1:]...) } else { - idents, ok := matches[retTyp] + names, ok := matches[retTyp] if !ok { return nil, fmt.Errorf("invalid return type: %v", retTyp) } - // Find the identifier whose name is most similar to the return type. - // If we do not find any identifier that matches the pattern, - // generate a zero value. - value := analysisinternal.FindBestMatch(retTyp.String(), idents) - if value == nil { - value = analysisinternal.ZeroValue(file, pass.Pkg, retTyp) - } - if value == nil { + // Find the identifier most similar to the return type. + // 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 := analysisinternal.ZeroValue(file, pass.Pkg, retTyp); zero != nil { + fixed[i] = zero + } else { return nil, nil } - fixed[i] = value } } diff --git a/internal/lsp/analysis/fillreturns/fillreturns_test.go b/gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go similarity index 89% rename from internal/lsp/analysis/fillreturns/fillreturns_test.go rename to gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go index 7ef0d46792e..1f7627551a0 100644 --- a/internal/lsp/analysis/fillreturns/fillreturns_test.go +++ b/gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/fillreturns" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/fillreturns/testdata/src/a/a.go b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/fillreturns/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go diff --git a/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go similarity index 100% rename from internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go rename to gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go diff --git a/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden similarity index 100% rename from internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden rename to gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden diff --git a/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go similarity index 78% rename from internal/lsp/analysis/fillstruct/fillstruct.go rename to gopls/internal/lsp/analysis/fillstruct/fillstruct.go index f160d4422ae..faf5ba5a9a6 100644 --- a/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -4,6 +4,12 @@ // Package fillstruct defines an Analyzer that automatically // fills in a struct declaration with zero value elements for each field. +// +// The analyzer's diagnostic is merely a prompt. +// The actual fix is created by a separate direct call from gopls to +// the SuggestedFixes function. +// Tests of Analyzer.Run can be found in ./testdata/src. +// Tests of the SuggestedFixes logic live in ../../testdata/fillstruct. package fillstruct import ( @@ -20,8 +26,9 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/typeparams" ) @@ -45,12 +52,10 @@ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} inspect.Preorder(nodeFilter, func(n ast.Node) { - info := pass.TypesInfo - if info == nil { - return - } expr := n.(*ast.CompositeLit) + // Find enclosing file. + // TODO(adonovan): use inspect.WithStack? var file *ast.File for _, f := range pass.Files { if f.Pos() <= expr.Pos() && expr.Pos() <= f.End() { @@ -62,73 +67,49 @@ func run(pass *analysis.Pass) (interface{}, error) { return } - typ := info.TypeOf(expr) + typ := pass.TypesInfo.TypeOf(expr) if typ == nil { return } - // Ignore types that have type parameters for now. - // TODO: support type params. - if typ, ok := typ.(*types.Named); ok { - if tparams := typeparams.ForNamed(typ); tparams != nil && tparams.Len() > 0 { - return - } - } - // Find reference to the type declaration of the struct being initialized. - for { - p, ok := typ.Underlying().(*types.Pointer) - if !ok { - break - } - typ = p.Elem() - } - typ = typ.Underlying() - - obj, ok := typ.(*types.Struct) + typ = deref(typ) + tStruct, ok := typ.Underlying().(*types.Struct) if !ok { return } - fieldCount := obj.NumFields() + // Inv: typ is the possibly-named struct type. + + fieldCount := tStruct.NumFields() // Skip any struct that is already populated or that has no fields. if fieldCount == 0 || fieldCount == len(expr.Elts) { return } - var fillable bool + // Are any fields in need of filling? var fillableFields []string for i := 0; i < fieldCount; i++ { - field := obj.Field(i) + field := tStruct.Field(i) // Ignore fields that are not accessible in the current package. if field.Pkg() != nil && field.Pkg() != pass.Pkg && !field.Exported() { continue } - // Ignore structs containing fields that have type parameters for now. - // TODO: support type params. - if typ, ok := field.Type().(*types.Named); ok { - if tparams := typeparams.ForNamed(typ); tparams != nil && tparams.Len() > 0 { - return - } - } - if _, ok := field.Type().(*typeparams.TypeParam); ok { - return - } - fillable = true fillableFields = append(fillableFields, fmt.Sprintf("%s: %s", field.Name(), field.Type().String())) } - if !fillable { + if len(fillableFields) == 0 { return } + + // Derive a name for the struct type. var name string - switch typ := expr.Type.(type) { - case *ast.Ident: - name = typ.Name - case *ast.SelectorExpr: - name = fmt.Sprintf("%s.%s", typ.X, typ.Sel.Name) - default: + if typ != tStruct { + // named struct type (e.g. pkg.S[T]) + name = types.TypeString(typ, types.RelativeTo(pass.Pkg)) + } else { + // anonymous struct type totalFields := len(fillableFields) - maxLen := 20 + const maxLen = 20 // Find the index to cut off printing of fields. var i, fieldLen int for i = range fillableFields { @@ -152,7 +133,13 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } +// SuggestedFix computes the suggested fix for the kinds of +// diagnostics produced by the Analyzer above. func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { + if info == nil { + return nil, fmt.Errorf("nil types.Info") + } + pos := rng.Start // don't use the end // TODO(rstambler): Using ast.Inspect would probably be more efficient than @@ -169,37 +156,29 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast } } - if info == nil { - return nil, fmt.Errorf("nil types.Info") - } typ := info.TypeOf(expr) if typ == nil { return nil, fmt.Errorf("no composite literal") } // Find reference to the type declaration of the struct being initialized. - for { - p, ok := typ.Underlying().(*types.Pointer) - if !ok { - break - } - typ = p.Elem() - } - typ = typ.Underlying() - - obj, ok := typ.(*types.Struct) + typ = deref(typ) + tStruct, ok := typ.Underlying().(*types.Struct) if !ok { - return nil, fmt.Errorf("unexpected type %v (%T), expected *types.Struct", typ, typ) + return nil, fmt.Errorf("%s is not a (pointer to) struct type", + types.TypeString(typ, types.RelativeTo(pkg))) } - fieldCount := obj.NumFields() + // Inv: typ is the the possibly-named struct type. + + fieldCount := tStruct.NumFields() // Check which types have already been filled in. (we only want to fill in // the unfilled types, or else we'll blat user-supplied details) - prefilledTypes := map[string]ast.Expr{} + prefilledFields := map[string]ast.Expr{} for _, e := range expr.Elts { if kv, ok := e.(*ast.KeyValueExpr); ok { if key, ok := kv.Key.(*ast.Ident); ok { - prefilledTypes[key.Name] = kv.Value + prefilledFields[key.Name] = kv.Value } } } @@ -209,14 +188,16 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast // each field we're going to set. format.Node only cares about line // numbers, so we don't need to set columns, and each line can be // 1 byte long. + // TODO(adonovan): why is this necessary? The position information + // is going to be wrong for the existing trees in prefilledFields. + // Can't the formatter just do its best with an empty fileset? fakeFset := token.NewFileSet() tok := fakeFset.AddFile("", -1, fieldCount+2) line := 2 // account for 1-based lines and the left brace - var elts []ast.Expr var fieldTyps []types.Type for i := 0; i < fieldCount; i++ { - field := obj.Field(i) + field := tStruct.Field(i) // Ignore fields that are not accessible in the current package. if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { fieldTyps = append(fieldTyps, nil) @@ -224,11 +205,13 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast } fieldTyps = append(fieldTyps, field.Type()) } - matches := analysisinternal.FindMatchingIdents(fieldTyps, file, rng.Start, info, pkg) + matches := analysisinternal.MatchingIdents(fieldTyps, file, rng.Start, info, pkg) + var elts []ast.Expr for i, fieldTyp := range fieldTyps { if fieldTyp == nil { - continue + continue // TODO(adonovan): is this reachable? } + fieldName := tStruct.Field(i).Name() tok.AddLine(line - 1) // add 1 byte per line if line > tok.LineCount() { @@ -239,30 +222,28 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast kv := &ast.KeyValueExpr{ Key: &ast.Ident{ NamePos: pos, - Name: obj.Field(i).Name(), + Name: fieldName, }, Colon: pos, } - if expr, ok := prefilledTypes[obj.Field(i).Name()]; ok { + if expr, ok := prefilledFields[fieldName]; ok { kv.Value = expr } else { - idents, ok := matches[fieldTyp] + names, ok := matches[fieldTyp] if !ok { return nil, fmt.Errorf("invalid struct field type: %v", fieldTyp) } - // Find the identifier whose name is most similar to the name of the field's key. - // If we do not find any identifier that matches the pattern, generate a new value. + // Find the name most similar to the field name. + // If no name matches the pattern, generate a zero value. // NOTE: We currently match on the name of the field key rather than the field type. - value := analysisinternal.FindBestMatch(obj.Field(i).Name(), idents) - if value == nil { - value = populateValue(file, pkg, fieldTyp) - } - if value == nil { + 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 { return nil, nil } - - kv.Value = value } elts = append(elts, kv) line++ @@ -306,7 +287,7 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast } sug := indent(formatBuf.Bytes(), whitespace) - if len(prefilledTypes) > 0 { + if len(prefilledFields) > 0 { // Attempt a second pass through the formatter to line up columns. sourced, err := format.Source(sug) if err == nil { @@ -350,16 +331,12 @@ func indent(str, ind []byte) []byte { // // When the type of a struct field is a basic literal or interface, we return // default values. For other types, such as maps, slices, and channels, we create -// expressions rather than using default values. +// empty expressions such as []T{} or make(chan T) rather than using default values. // // 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. func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { - under := typ - if n, ok := typ.(*types.Named); ok { - under = n.Underlying() - } - switch u := under.(type) { + switch u := typ.Underlying().(type) { case *types.Basic: switch { case u.Info()&types.IsNumeric != 0: @@ -373,6 +350,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { default: panic("unknown basic type") } + case *types.Map: k := analysisinternal.TypeExpr(f, pkg, u.Key()) v := analysisinternal.TypeExpr(f, pkg, u.Elem()) @@ -395,6 +373,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { Elt: s, }, } + case *types.Array: a := analysisinternal.TypeExpr(f, pkg, u.Elem()) if a == nil { @@ -408,6 +387,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { }, }, } + case *types.Chan: v := analysisinternal.TypeExpr(f, pkg, u.Elem()) if v == nil { @@ -426,6 +406,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { }, }, } + case *types.Struct: s := analysisinternal.TypeExpr(f, pkg, typ) if s == nil { @@ -434,6 +415,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { return &ast.CompositeLit{ Type: s, } + case *types.Signature: var params []*ast.Field for i := 0; i < u.Params().Len(); i++ { @@ -471,6 +453,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { }, Body: &ast.BlockStmt{}, } + case *types.Pointer: switch u.Elem().(type) { case *types.Basic: @@ -490,8 +473,34 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { X: populateValue(f, pkg, u.Elem()), } } + case *types.Interface: + if param, ok := typ.(*typeparams.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()), + }, + }, + } + } + return ast.NewIdent("nil") } return nil } + +func deref(t types.Type) types.Type { + for { + ptr, ok := t.Underlying().(*types.Pointer) + if !ok { + return t + } + t = ptr.Elem() + } +} diff --git a/internal/lsp/analysis/fillstruct/fillstruct_test.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go similarity index 89% rename from internal/lsp/analysis/fillstruct/fillstruct_test.go rename to gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go index 51a516cdfdb..66642b7ab59 100644 --- a/internal/lsp/analysis/fillstruct/fillstruct_test.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/fillstruct" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/fillstruct/testdata/src/a/a.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go similarity index 58% rename from internal/lsp/analysis/fillstruct/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go index 68560092105..9ee3860fcae 100644 --- a/internal/lsp/analysis/fillstruct/testdata/src/a/a.go +++ b/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go @@ -19,16 +19,16 @@ type basicStruct struct { foo int } -var _ = basicStruct{} // want "" +var _ = basicStruct{} // want `Fill basicStruct` type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} // want "" +var _ = twoArgStruct{} // want `Fill twoArgStruct` -var _ = twoArgStruct{ // want "" +var _ = twoArgStruct{ // want `Fill twoArgStruct` bar: "bar", } @@ -37,9 +37,9 @@ type nestedStruct struct { basic basicStruct } -var _ = nestedStruct{} // want "" +var _ = nestedStruct{} // want `Fill nestedStruct` -var _ = data.B{} // want "" +var _ = data.B{} // want `Fill b.B` type typedStruct struct { m map[string]int @@ -49,25 +49,25 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} // want "" +var _ = typedStruct{} // want `Fill typedStruct` type funStruct struct { fn func(i int) int } -var _ = funStruct{} // want "" +var _ = funStruct{} // want `Fill funStruct` -type funStructCompex struct { +type funStructComplex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} // want "" +var _ = funStructComplex{} // want `Fill funStructComplex` type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} // want "" +var _ = funStructEmpty{} // want `Fill funStructEmpty` type Foo struct { A int @@ -78,7 +78,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} // want "" +var _ = Bar{} // want `Fill Bar` type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -89,7 +89,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} // want "" +var _ = importedStruct{} // want `Fill importedStruct` type pointerBuiltinStruct struct { b *bool @@ -97,17 +97,17 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} // want "" +var _ = pointerBuiltinStruct{} // want `Fill pointerBuiltinStruct` var _ = []ast.BasicLit{ - {}, // want "" + {}, // want `Fill go/ast.BasicLit` } -var _ = []ast.BasicLit{{}, // want "" +var _ = []ast.BasicLit{{}, // want "go/ast.BasicLit" } type unsafeStruct struct { foo unsafe.Pointer } -var _ = unsafeStruct{} // want "" +var _ = unsafeStruct{} // want `Fill unsafeStruct` diff --git a/internal/lsp/analysis/fillstruct/testdata/src/b/b.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/b/b.go similarity index 100% rename from internal/lsp/analysis/fillstruct/testdata/src/b/b.go rename to gopls/internal/lsp/analysis/fillstruct/testdata/src/b/b.go diff --git a/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go new file mode 100644 index 00000000000..46bb8ae4027 --- /dev/null +++ b/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go @@ -0,0 +1,50 @@ +// 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 fillstruct + +type emptyStruct[A any] struct{} + +var _ = emptyStruct[int]{} + +type basicStruct[T any] struct { + foo T +} + +var _ = basicStruct[int]{} // want `Fill basicStruct\[int\]` + +type twoArgStruct[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStruct[string, int]{} // want `Fill twoArgStruct\[string, int\]` + +var _ = twoArgStruct[int, string]{ // want `Fill twoArgStruct\[int, string\]` + bar: "bar", +} + +type nestedStruct struct { + bar string + basic basicStruct[int] +} + +var _ = nestedStruct{} // want "Fill nestedStruct" + +func _[T any]() { + type S struct{ t T } + x := S{} // want "Fill S" + _ = x +} + +func Test() { + var tests = []struct { + a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p string + }{ + {}, // want "Fill anonymous struct { a: string, b: string, c: string, ... }" + } + for _, test := range tests { + _ = test + } +} diff --git a/internal/lsp/analysis/infertypeargs/infertypeargs.go b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/infertypeargs.go rename to gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go diff --git a/internal/lsp/analysis/infertypeargs/infertypeargs_test.go b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go similarity index 90% rename from internal/lsp/analysis/infertypeargs/infertypeargs_test.go rename to gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go index 2957f46e367..2d687f0e72a 100644 --- a/internal/lsp/analysis/infertypeargs/infertypeargs_test.go +++ b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/infertypeargs" + "golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/infertypeargs/run_go117.go b/gopls/internal/lsp/analysis/infertypeargs/run_go117.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/run_go117.go rename to gopls/internal/lsp/analysis/infertypeargs/run_go117.go diff --git a/internal/lsp/analysis/infertypeargs/run_go118.go b/gopls/internal/lsp/analysis/infertypeargs/run_go118.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/run_go118.go rename to gopls/internal/lsp/analysis/infertypeargs/run_go118.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go diff --git a/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden similarity index 100% rename from internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden rename to gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden diff --git a/internal/lsp/analysis/nonewvars/nonewvars.go b/gopls/internal/lsp/analysis/nonewvars/nonewvars.go similarity index 100% rename from internal/lsp/analysis/nonewvars/nonewvars.go rename to gopls/internal/lsp/analysis/nonewvars/nonewvars.go diff --git a/internal/lsp/analysis/nonewvars/nonewvars_test.go b/gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go similarity index 89% rename from internal/lsp/analysis/nonewvars/nonewvars_test.go rename to gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go index dc58ab0ff5e..8f6f0a51fb4 100644 --- a/internal/lsp/analysis/nonewvars/nonewvars_test.go +++ b/gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/nonewvars" + "golang.org/x/tools/gopls/internal/lsp/analysis/nonewvars" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/nonewvars/testdata/src/a/a.go b/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/nonewvars/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go diff --git a/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go b/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go similarity index 100% rename from internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go rename to gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go diff --git a/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden b/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden similarity index 100% rename from internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden rename to gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden diff --git a/internal/lsp/analysis/noresultvalues/noresultvalues.go b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go similarity index 100% rename from internal/lsp/analysis/noresultvalues/noresultvalues.go rename to gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go diff --git a/internal/lsp/analysis/noresultvalues/noresultvalues_test.go b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go similarity index 89% rename from internal/lsp/analysis/noresultvalues/noresultvalues_test.go rename to gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go index 12198a1c130..24ce39207ee 100644 --- a/internal/lsp/analysis/noresultvalues/noresultvalues_test.go +++ b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/noresultvalues" + "golang.org/x/tools/gopls/internal/lsp/analysis/noresultvalues" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/noresultvalues/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go diff --git a/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go similarity index 100% rename from internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go rename to gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go diff --git a/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden similarity index 100% rename from internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden rename to gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden diff --git a/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go similarity index 100% rename from internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go rename to gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go diff --git a/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go similarity index 85% rename from internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go rename to gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go index e60f7d6b055..b0365a6b3da 100644 --- a/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go +++ b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/simplifycompositelit" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifycompositelit" ) func Test(t *testing.T) { diff --git a/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go diff --git a/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/simplifyrange/simplifyrange.go b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange.go similarity index 100% rename from internal/lsp/analysis/simplifyrange/simplifyrange.go rename to gopls/internal/lsp/analysis/simplifyrange/simplifyrange.go diff --git a/internal/lsp/analysis/simplifyrange/simplifyrange_test.go b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go similarity index 86% rename from internal/lsp/analysis/simplifyrange/simplifyrange_test.go rename to gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go index ecc7a969257..fbd57ec2d65 100644 --- a/internal/lsp/analysis/simplifyrange/simplifyrange_test.go +++ b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/simplifyrange" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyrange" ) func Test(t *testing.T) { diff --git a/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/simplifyrange/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go diff --git a/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/simplifyslice/simplifyslice.go b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice.go similarity index 100% rename from internal/lsp/analysis/simplifyslice/simplifyslice.go rename to gopls/internal/lsp/analysis/simplifyslice/simplifyslice.go diff --git a/internal/lsp/analysis/simplifyslice/simplifyslice_test.go b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go similarity index 89% rename from internal/lsp/analysis/simplifyslice/simplifyslice_test.go rename to gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go index cff6267c679..41914ba3170 100644 --- a/internal/lsp/analysis/simplifyslice/simplifyslice_test.go +++ b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/simplifyslice" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyslice" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/simplifyslice/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go diff --git a/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go similarity index 100% rename from internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go rename to gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go diff --git a/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden similarity index 100% rename from internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden rename to gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden diff --git a/internal/lsp/analysis/stubmethods/stubmethods.go b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go similarity index 100% rename from internal/lsp/analysis/stubmethods/stubmethods.go rename to gopls/internal/lsp/analysis/stubmethods/stubmethods.go diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go similarity index 58% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go index 81c732001af..c5d8a2d789c 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go @@ -6,22 +6,22 @@ package undeclared func x() int { var z int - z = y // want "undeclared name: y" + z = y // want "(undeclared name|undefined): y" - if z == m { // want "undeclared name: m" + if z == m { // want "(undeclared name|undefined): m" z = 1 } if z == 1 { z = 1 - } else if z == n+1 { // want "undeclared name: n" + } else if z == n+1 { // want "(undeclared name|undefined): n" z = 1 } switch z { case 10: z = 1 - case a: // want "undeclared name: a" + case a: // want "(undeclared name|undefined): a" z = 1 } return z diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go similarity index 78% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go index ecf00ecfc20..76c7ba685e1 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go @@ -5,7 +5,7 @@ package undeclared func channels(s string) { - undefinedChannels(c()) // want "undeclared name: undefinedChannels" + undefinedChannels(c()) // want "(undeclared name|undefined): undefinedChannels" } func c() (<-chan string, chan string) { diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go similarity index 69% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go index ab7b2ba5c18..73beace102c 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go @@ -6,5 +6,5 @@ package undeclared func consecutiveParams() { var s string - undefinedConsecutiveParams(s, s) // want "undeclared name: undefinedConsecutiveParams" + undefinedConsecutiveParams(s, s) // want "(undeclared name|undefined): undefinedConsecutiveParams" } diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go similarity index 71% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go index 341a9d2a453..5de9254112d 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go @@ -6,5 +6,5 @@ package undeclared func errorParam() { var err error - undefinedErrorParam(err) // want "undeclared name: undefinedErrorParam" + undefinedErrorParam(err) // want "(undeclared name|undefined): undefinedErrorParam" } diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go similarity index 67% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go index ab82463d00e..c62174ec947 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go @@ -7,5 +7,5 @@ package undeclared type T struct{} func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) // want "undeclared name: undefinedLiterals" + undefinedLiterals("hey compiler", T{}, &T{}) // want "(undeclared name|undefined): undefinedLiterals" } diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go similarity index 69% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go index 9a543821ee6..9396da4bd9d 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go @@ -7,5 +7,5 @@ package undeclared import "time" func operation() { - undefinedOperation(10 * time.Second) // want "undeclared name: undefinedOperation" + undefinedOperation(10 * time.Second) // want "(undeclared name|undefined): undefinedOperation" } diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go similarity index 72% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go index 9ed09a27f24..a4ed290d466 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go @@ -6,5 +6,5 @@ package undeclared func selector() { m := map[int]bool{} - undefinedSelector(m[1]) // want "undeclared name: undefinedSelector" + undefinedSelector(m[1]) // want "(undeclared name|undefined): undefinedSelector" } diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go similarity index 70% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go index d741c68f68d..5cde299add3 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go @@ -5,5 +5,5 @@ package undeclared func slice() { - undefinedSlice([]int{1, 2}) // want "undeclared name: undefinedSlice" + undefinedSlice([]int{1, 2}) // want "(undeclared name|undefined): undefinedSlice" } diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go similarity index 76% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go index 3148e8f4d4c..9e91c59c25e 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go @@ -5,7 +5,7 @@ package undeclared func tuple() { - undefinedTuple(b()) // want "undeclared name: undefinedTuple" + undefinedTuple(b()) // want "(undeclared name|undefined): undefinedTuple" } func b() (string, error) { diff --git a/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go similarity index 70% rename from internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go rename to gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go index 98f77a43cd1..5b4241425e5 100644 --- a/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go +++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go @@ -7,5 +7,5 @@ package undeclared func uniqueArguments() { var s string var i int - undefinedUniqueArguments(s, i, s) // want "undeclared name: undefinedUniqueArguments" + undefinedUniqueArguments(s, i, s) // want "(undeclared name|undefined): undefinedUniqueArguments" } diff --git a/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go similarity index 95% rename from internal/lsp/analysis/undeclaredname/undeclared.go rename to gopls/internal/lsp/analysis/undeclaredname/undeclared.go index faa14091aee..fd8d35eb1b9 100644 --- a/internal/lsp/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go @@ -18,8 +18,8 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/span" ) const Doc = `suggested fixes for "undeclared name: <>" @@ -45,7 +45,8 @@ var Analyzer = &analysis.Analyzer{ RunDespiteErrors: true, } -const undeclaredNamePrefix = "undeclared name: " +// The prefix for this error message changed in Go 1.20. +var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "} func run(pass *analysis.Pass) (interface{}, error) { for _, err := range analysisinternal.GetTypeErrors(pass) { @@ -55,10 +56,16 @@ func run(pass *analysis.Pass) (interface{}, error) { } func runForError(pass *analysis.Pass, err types.Error) { - if !strings.HasPrefix(err.Msg, undeclaredNamePrefix) { + var name string + for _, prefix := range undeclaredNamePrefixes { + if !strings.HasPrefix(err.Msg, prefix) { + continue + } + name = strings.TrimPrefix(err.Msg, prefix) + } + if name == "" { return } - name := strings.TrimPrefix(err.Msg, undeclaredNamePrefix) var file *ast.File for _, f := range pass.Files { if f.Pos() <= err.Pos && err.Pos < f.End() { diff --git a/internal/lsp/analysis/undeclaredname/undeclared_test.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go similarity index 85% rename from internal/lsp/analysis/undeclaredname/undeclared_test.go rename to gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go index b7154393742..306c3f03941 100644 --- a/internal/lsp/analysis/undeclaredname/undeclared_test.go +++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/undeclaredname" + "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" ) func Test(t *testing.T) { diff --git a/internal/lsp/analysis/unusedparams/testdata/src/a/a.go b/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/unusedparams/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go diff --git a/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go similarity index 100% rename from internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go rename to gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go diff --git a/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden similarity index 100% rename from internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden rename to gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden diff --git a/internal/lsp/analysis/unusedparams/unusedparams.go b/gopls/internal/lsp/analysis/unusedparams/unusedparams.go similarity index 100% rename from internal/lsp/analysis/unusedparams/unusedparams.go rename to gopls/internal/lsp/analysis/unusedparams/unusedparams.go diff --git a/internal/lsp/analysis/unusedparams/unusedparams_test.go b/gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go similarity index 89% rename from internal/lsp/analysis/unusedparams/unusedparams_test.go rename to gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go index dff17c95e5d..fdd43b821fe 100644 --- a/internal/lsp/analysis/unusedparams/unusedparams_test.go +++ b/gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/unusedparams" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams" "golang.org/x/tools/internal/typeparams" ) diff --git a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go new file mode 100644 index 00000000000..aa9f46e5b31 --- /dev/null +++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go @@ -0,0 +1,74 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package a + +import ( + "fmt" + "os" +) + +type A struct { + b int +} + +func singleAssignment() { + v := "s" // want `v declared (and|but) not used` + + s := []int{ // want `s declared (and|but) not used` + 1, + 2, + } + + a := func(s string) bool { // want `a declared (and|but) not used` + return false + } + + if 1 == 1 { + s := "v" // want `s declared (and|but) not used` + } + + panic("I should survive") +} + +func noOtherStmtsInBlock() { + v := "s" // want `v declared (and|but) not used` +} + +func partOfMultiAssignment() { + f, err := os.Open("file") // want `f declared (and|but) not used` + panic(err) +} + +func sideEffects(cBool chan bool, cInt chan int) { + b := <-c // want `b declared (and|but) not used` + s := fmt.Sprint("") // want `s declared (and|but) not used` + a := A{ // want `a declared (and|but) not used` + b: func() int { + return 1 + }(), + } + c := A{<-cInt} // want `c declared (and|but) not used` + d := fInt() + <-cInt // want `d declared (and|but) not used` + e := fBool() && <-cBool // want `e declared (and|but) not used` + f := map[int]int{ // want `f declared (and|but) not used` + fInt(): <-cInt, + } + g := []int{<-cInt} // want `g declared (and|but) not used` + h := func(s string) {} // want `h declared (and|but) not used` + i := func(s string) {}() // want `i declared (and|but) not used` +} + +func commentAbove() { + // v is a variable + v := "s" // want `v declared (and|but) not used` +} + +func fBool() bool { + return true +} + +func fInt() int { + return 1 +} diff --git a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden new file mode 100644 index 00000000000..18173ce0bf9 --- /dev/null +++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden @@ -0,0 +1,59 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package a + +import ( + "fmt" + "os" +) + +type A struct { + b int +} + +func singleAssignment() { + if 1 == 1 { + } + + panic("I should survive") +} + +func noOtherStmtsInBlock() { +} + +func partOfMultiAssignment() { + _, err := os.Open("file") // want `f declared (and|but) not used` + panic(err) +} + +func sideEffects(cBool chan bool, cInt chan int) { + <-c // want `b declared (and|but) not used` + fmt.Sprint("") // want `s declared (and|but) not used` + A{ // want `a declared (and|but) not used` + b: func() int { + return 1 + }(), + } + A{<-cInt} // want `c declared (and|but) not used` + fInt() + <-cInt // want `d declared (and|but) not used` + fBool() && <-cBool // want `e declared (and|but) not used` + map[int]int{ // want `f declared (and|but) not used` + fInt(): <-cInt, + } + []int{<-cInt} // want `g declared (and|but) not used` + func(s string) {}() // want `i declared (and|but) not used` +} + +func commentAbove() { + // v is a variable +} + +func fBool() bool { + return true +} + +func fInt() int { + return 1 +} diff --git a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go new file mode 100644 index 00000000000..8e843024a54 --- /dev/null +++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go @@ -0,0 +1,30 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package decl + +func a() { + var b, c bool // want `b declared (and|but) not used` + panic(c) + + if 1 == 1 { + var s string // want `s declared (and|but) not used` + } +} + +func b() { + // b is a variable + var b bool // want `b declared (and|but) not used` +} + +func c() { + var ( + d string + + // some comment for c + c bool // want `c declared (and|but) not used` + ) + + panic(d) +} diff --git a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden new file mode 100644 index 00000000000..6ed97332eea --- /dev/null +++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden @@ -0,0 +1,24 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package decl + +func a() { + var c bool // want `b declared (and|but) not used` + panic(c) + + if 1 == 1 { + } +} + +func b() { + // b is a variable +} + +func c() { + var ( + d string + ) + panic(d) +} diff --git a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go new file mode 100644 index 00000000000..134cbb2c436 --- /dev/null +++ b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go @@ -0,0 +1,301 @@ +// 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 unusedvariable defines an analyzer that checks for unused variables. +package unusedvariable + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/internal/analysisinternal" +) + +const Doc = `check for unused variables + +The unusedvariable analyzer suggests fixes for unused variables errors. +` + +var Analyzer = &analysis.Analyzer{ + Name: "unusedvariable", + Doc: Doc, + Requires: []*analysis.Analyzer{}, + Run: run, + RunDespiteErrors: true, // an unusedvariable diagnostic is a compile error +} + +// The suffix for this error message changed in Go 1.20. +var unusedVariableSuffixes = []string{" declared and not used", " declared but not used"} + +func run(pass *analysis.Pass) (interface{}, error) { + for _, typeErr := range analysisinternal.GetTypeErrors(pass) { + for _, suffix := range unusedVariableSuffixes { + if strings.HasSuffix(typeErr.Msg, suffix) { + varName := strings.TrimSuffix(typeErr.Msg, suffix) + err := runForError(pass, typeErr, varName) + if err != nil { + return nil, err + } + } + } + } + + return nil, nil +} + +func runForError(pass *analysis.Pass, err types.Error, name string) error { + var file *ast.File + for _, f := range pass.Files { + if f.Pos() <= err.Pos && err.Pos < f.End() { + file = f + break + } + } + if file == nil { + return nil + } + + path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) + if len(path) < 2 { + return nil + } + + ident, ok := path[0].(*ast.Ident) + if !ok || ident.Name != name { + return nil + } + + diag := analysis.Diagnostic{ + Pos: ident.Pos(), + End: ident.End(), + Message: err.Msg, + } + + for i := range path { + switch stmt := path[i].(type) { + case *ast.ValueSpec: + // Find GenDecl to which offending ValueSpec belongs. + if decl, ok := path[i+1].(*ast.GenDecl); ok { + fixes := removeVariableFromSpec(pass, path, stmt, decl, ident) + // fixes may be nil + if len(fixes) > 0 { + diag.SuggestedFixes = fixes + pass.Report(diag) + } + } + + case *ast.AssignStmt: + if stmt.Tok != token.DEFINE { + continue + } + + containsIdent := false + for _, expr := range stmt.Lhs { + if expr == ident { + containsIdent = true + } + } + if !containsIdent { + continue + } + + fixes := removeVariableFromAssignment(pass, path, stmt, ident) + // fixes may be nil + if len(fixes) > 0 { + diag.SuggestedFixes = fixes + pass.Report(diag) + } + } + } + + return nil +} + +func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.ValueSpec, decl *ast.GenDecl, ident *ast.Ident) []analysis.SuggestedFix { + newDecl := new(ast.GenDecl) + *newDecl = *decl + newDecl.Specs = nil + + for _, spec := range decl.Specs { + if spec != stmt { + newDecl.Specs = append(newDecl.Specs, spec) + continue + } + + newSpec := new(ast.ValueSpec) + *newSpec = *stmt + newSpec.Names = nil + + for _, n := range stmt.Names { + if n != ident { + newSpec.Names = append(newSpec.Names, n) + } + } + + if len(newSpec.Names) > 0 { + newDecl.Specs = append(newDecl.Specs, newSpec) + } + } + + // decl.End() does not include any comments, so if a comment is present we + // need to account for it when we delete the statement + end := decl.End() + if stmt.Comment != nil && stmt.Comment.End() > end { + end = stmt.Comment.End() + } + + // There are no other specs left in the declaration, the whole statement can + // be deleted + if len(newDecl.Specs) == 0 { + // Find parent DeclStmt and delete it + for _, node := range path { + if declStmt, ok := node.(*ast.DeclStmt); ok { + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: deleteStmtFromBlock(path, declStmt), + }, + } + } + } + } + + var b bytes.Buffer + if err := format.Node(&b, pass.Fset, newDecl); err != nil { + return nil + } + + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: []analysis.TextEdit{ + { + Pos: decl.Pos(), + // Avoid adding a new empty line + End: end + 1, + NewText: b.Bytes(), + }, + }, + }, + } +} + +func removeVariableFromAssignment(pass *analysis.Pass, path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix { + // The only variable in the assignment is unused + if len(stmt.Lhs) == 1 { + // If LHS has only one expression to be valid it has to have 1 expression + // on RHS + // + // RHS may have side effects, preserve RHS + if exprMayHaveSideEffects(stmt.Rhs[0]) { + // Delete until RHS + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: []analysis.TextEdit{ + { + Pos: ident.Pos(), + End: stmt.Rhs[0].Pos(), + }, + }, + }, + } + } + + // RHS does not have any side effects, delete the whole statement + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: deleteStmtFromBlock(path, stmt), + }, + } + } + + // Otherwise replace ident with `_` + return []analysis.SuggestedFix{ + { + Message: suggestedFixMessage(ident.Name), + TextEdits: []analysis.TextEdit{ + { + Pos: ident.Pos(), + End: ident.End(), + NewText: []byte("_"), + }, + }, + }, + } +} + +func suggestedFixMessage(name string) string { + return fmt.Sprintf("Remove variable %s", name) +} + +func deleteStmtFromBlock(path []ast.Node, stmt ast.Stmt) []analysis.TextEdit { + // Find innermost enclosing BlockStmt. + var block *ast.BlockStmt + for i := range path { + if blockStmt, ok := path[i].(*ast.BlockStmt); ok { + block = blockStmt + break + } + } + + nodeIndex := -1 + for i, blockStmt := range block.List { + if blockStmt == stmt { + nodeIndex = i + break + } + } + + // The statement we need to delete was not found in BlockStmt + if nodeIndex == -1 { + return nil + } + + // Delete until the end of the block unless there is another statement after + // the one we are trying to delete + end := block.Rbrace + if nodeIndex < len(block.List)-1 { + end = block.List[nodeIndex+1].Pos() + } + + return []analysis.TextEdit{ + { + Pos: stmt.Pos(), + End: end, + }, + } +} + +// exprMayHaveSideEffects reports whether the expression may have side effects +// (because it contains a function call or channel receive). We disregard +// runtime panics as well written programs should not encounter them. +func exprMayHaveSideEffects(expr ast.Expr) bool { + var mayHaveSideEffects bool + ast.Inspect(expr, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.CallExpr: // possible function call + mayHaveSideEffects = true + return false + case *ast.UnaryExpr: + if n.Op == token.ARROW { // channel receive + mayHaveSideEffects = true + return false + } + case *ast.FuncLit: + return false // evaluating what's inside a FuncLit has no effect + } + return true + }) + + return mayHaveSideEffects +} diff --git a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go new file mode 100644 index 00000000000..08223155f6e --- /dev/null +++ b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go @@ -0,0 +1,24 @@ +// 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 unusedvariable_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedvariable" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + + t.Run("decl", func(t *testing.T) { + analysistest.RunWithSuggestedFixes(t, testdata, unusedvariable.Analyzer, "decl") + }) + + t.Run("assign", func(t *testing.T) { + analysistest.RunWithSuggestedFixes(t, testdata, unusedvariable.Analyzer, "assign") + }) +} diff --git a/internal/lsp/analysis/useany/testdata/src/a/a.go b/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go similarity index 100% rename from internal/lsp/analysis/useany/testdata/src/a/a.go rename to gopls/internal/lsp/analysis/useany/testdata/src/a/a.go diff --git a/internal/lsp/analysis/useany/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go.golden similarity index 100% rename from internal/lsp/analysis/useany/testdata/src/a/a.go.golden rename to gopls/internal/lsp/analysis/useany/testdata/src/a/a.go.golden diff --git a/internal/lsp/analysis/useany/useany.go b/gopls/internal/lsp/analysis/useany/useany.go similarity index 100% rename from internal/lsp/analysis/useany/useany.go rename to gopls/internal/lsp/analysis/useany/useany.go diff --git a/internal/lsp/analysis/useany/useany_test.go b/gopls/internal/lsp/analysis/useany/useany_test.go similarity index 89% rename from internal/lsp/analysis/useany/useany_test.go rename to gopls/internal/lsp/analysis/useany/useany_test.go index 535d9152665..083c3d54fd4 100644 --- a/internal/lsp/analysis/useany/useany_test.go +++ b/gopls/internal/lsp/analysis/useany/useany_test.go @@ -8,7 +8,7 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/lsp/analysis/useany" + "golang.org/x/tools/gopls/internal/lsp/analysis/useany" "golang.org/x/tools/internal/typeparams" ) diff --git a/internal/lsp/browser/README.md b/gopls/internal/lsp/browser/README.md similarity index 100% rename from internal/lsp/browser/README.md rename to gopls/internal/lsp/browser/README.md diff --git a/internal/lsp/browser/browser.go b/gopls/internal/lsp/browser/browser.go similarity index 100% rename from internal/lsp/browser/browser.go rename to gopls/internal/lsp/browser/browser.go diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go new file mode 100644 index 00000000000..e15b43e91ce --- /dev/null +++ b/gopls/internal/lsp/cache/analysis.go @@ -0,0 +1,502 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "context" + "fmt" + "go/ast" + "go/types" + "reflect" + "runtime/debug" + "sync" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/memoize" +) + +func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) { + // TODO(adonovan): merge these two loops. There's no need to + // construct all the root action handles before beginning + // analysis. Operations should be concurrent (though that first + // requires buildPackageHandle not to be inefficient when + // called in parallel.) + var roots []*actionHandle + for _, a := range analyzers { + if !a.IsEnabled(s.view) { + continue + } + ah, err := s.actionHandle(ctx, PackageID(id), a.Analyzer) + if err != nil { + return nil, err + } + roots = append(roots, ah) + } + + // Run and wait for all analyzers, and report diagnostics + // only from those that succeed. Ignore the others. + var results []*source.Diagnostic + for _, ah := range roots { + v, err := s.awaitPromise(ctx, ah.promise) + if err != nil { + return nil, err // wait was cancelled + } + + res := v.(actionResult) + if res.err != nil { + continue // analysis failed; ignore it. + } + + results = append(results, res.data.diagnostics...) + } + return results, nil +} + +type actionKey struct { + pkgid PackageID + analyzer *analysis.Analyzer +} + +// An action represents one unit of analysis work: the application of +// one analysis to one package. Actions form a DAG, both within a +// package (as different analyzers are applied, either in sequence or +// parallel), and across packages (as dependencies are analyzed). +type actionHandle struct { + key actionKey // just for String() + promise *memoize.Promise // [actionResult] +} + +// actionData is the successful result of analyzing a package. +type actionData struct { + analyzer *analysis.Analyzer + pkgTypes *types.Package // types only; don't keep syntax live + diagnostics []*source.Diagnostic + result interface{} + objectFacts map[objectFactKey]analysis.Fact + packageFacts map[packageFactKey]analysis.Fact +} + +// actionResult holds the result of a call to actionImpl. +type actionResult struct { + data *actionData + err error +} + +type objectFactKey struct { + obj types.Object + typ reflect.Type +} + +type packageFactKey struct { + pkg *types.Package + typ reflect.Type +} + +func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.Analyzer) (*actionHandle, error) { + key := actionKey{id, a} + + s.mu.Lock() + entry, hit := s.actions.Get(key) + s.mu.Unlock() + + if hit { + return entry.(*actionHandle), nil + } + + // TODO(adonovan): opt: this block of code sequentially loads a package + // (and all its dependencies), then sequentially creates action handles + // for the direct dependencies (whose packages have by then been loaded + // as a consequence of ph.check) which does a sequential recursion + // down the action graph. Only once all that work is complete do we + // put a handle in the cache. As with buildPackageHandle, this does + // not exploit the natural parallelism in the problem, and the naive + // use of concurrency would lead to an exponential amount of duplicated + // work. We should instead use an atomically updated future cache + // and a parallel graph traversal. + ph, err := s.buildPackageHandle(ctx, id, source.ParseFull) + if err != nil { + return nil, err + } + pkg, err := ph.await(ctx, s) + if err != nil { + return nil, err + } + + // Add a dependency on each required analyzer. + var deps []*actionHandle // unordered + for _, req := range a.Requires { + // TODO(adonovan): opt: there's no need to repeat the package-handle + // portion of the recursion here, since we have the pkg already. + reqActionHandle, err := s.actionHandle(ctx, id, req) + if err != nil { + return nil, err + } + deps = append(deps, reqActionHandle) + } + + // TODO(golang/go#35089): Re-enable this when we doesn't use ParseExported + // mode for dependencies. In the meantime, disable analysis for dependencies, + // since we don't get anything useful out of it. + if false { + // An analysis that consumes/produces facts + // must run on the package's dependencies too. + if len(a.FactTypes) > 0 { + for _, depID := range ph.m.DepsByPkgPath { + depActionHandle, err := s.actionHandle(ctx, depID, a) + if err != nil { + return nil, err + } + deps = append(deps, depActionHandle) + } + } + } + + // The promises are kept in a store on the package, + // so the key need only include the analyzer name. + // + // (Since type-checking and analysis depend on the identity + // of packages--distinct packages produced by the same + // recipe are not fungible--we must in effect use the package + // itself as part of the key. Rather than actually use a pointer + // in the key, we get a simpler object graph if we shard the + // store by packages.) + promise, release := pkg.analyses.Promise(a.Name, func(ctx context.Context, arg interface{}) interface{} { + res, err := actionImpl(ctx, arg.(*snapshot), deps, a, pkg) + return actionResult{res, err} + }) + + ah := &actionHandle{ + key: key, + promise: promise, + } + + s.mu.Lock() + defer s.mu.Unlock() + + // Check cache again in case another thread got there first. + if result, ok := s.actions.Get(key); ok { + release() + return result.(*actionHandle), nil + } + + s.actions.Set(key, ah, func(_, _ interface{}) { release() }) + + return ah, nil +} + +func (key actionKey) String() string { + return fmt.Sprintf("%s@%s", key.analyzer, key.pkgid) +} + +func (act *actionHandle) String() string { + return act.key.String() +} + +// actionImpl runs the analysis for action node (analyzer, pkg), +// whose direct dependencies are deps. +func actionImpl(ctx context.Context, snapshot *snapshot, deps []*actionHandle, analyzer *analysis.Analyzer, pkg *pkg) (*actionData, error) { + // Run action dependencies first, and plumb the results and + // facts of each dependency into the inputs of this action. + var ( + mu sync.Mutex + inputs = make(map[*analysis.Analyzer]interface{}) + objectFacts = make(map[objectFactKey]analysis.Fact) + packageFacts = make(map[packageFactKey]analysis.Fact) + ) + g, ctx := errgroup.WithContext(ctx) + for _, dep := range deps { + dep := dep + g.Go(func() error { + v, err := snapshot.awaitPromise(ctx, dep.promise) + if err != nil { + return err // e.g. cancelled + } + res := v.(actionResult) + if res.err != nil { + return res.err // analysis of dependency failed + } + data := res.data + + mu.Lock() + defer mu.Unlock() + if data.pkgTypes == pkg.types { + // Same package, different analysis (horizontal edge): + // in-memory outputs of prerequisite analyzers + // become inputs to this analysis pass. + inputs[data.analyzer] = data.result + + } else if data.analyzer == analyzer { + // Same analysis, different package (vertical edge): + // serialized facts produced by prerequisite analysis + // become available to this analysis pass. + for key, fact := range data.objectFacts { + objectFacts[key] = fact + } + for key, fact := range data.packageFacts { + packageFacts[key] = fact + } + + } else { + // Edge is neither vertical nor horizontal. + // This should never happen, yet an assertion here was + // observed to fail due to an edge (bools, p) -> (inspector, p') + // where p and p' are distinct packages with the + // same ID ("command-line-arguments:file=.../main.go"). + // + // It is not yet clear whether the command-line-arguments + // package is significant, but it is clear that package + // loading (the mapping from ID to *pkg) is inconsistent + // within a single graph. + + // Use the bug package so that we detect whether our tests + // discover this problem in regular packages. + // For command-line-arguments we quietly abort the analysis + // for now since we already know there is a bug. + errorf := bug.Errorf // report this discovery + if source.IsCommandLineArguments(pkg.ID()) { + errorf = fmt.Errorf // suppress reporting + } + err := errorf("internal error: unexpected analysis dependency %s@%s -> %s", analyzer.Name, pkg.ID(), dep) + // Log the event in any case, as the ultimate + // consumer of actionResult ignores errors. + event.Error(ctx, "analysis", err) + return err + } + return nil + }) + } + if err := g.Wait(); err != nil { + return nil, err // cancelled, or dependency failed + } + + // Now run the (pkg, analyzer) analysis. + var syntax []*ast.File + for _, cgf := range pkg.compiledGoFiles { + syntax = append(syntax, cgf.File) + } + var rawDiagnostics []analysis.Diagnostic + pass := &analysis.Pass{ + Analyzer: analyzer, + Fset: snapshot.FileSet(), + Files: syntax, + Pkg: pkg.GetTypes(), + TypesInfo: pkg.GetTypesInfo(), + TypesSizes: pkg.GetTypesSizes(), + ResultOf: inputs, + Report: func(d analysis.Diagnostic) { + // Prefix the diagnostic category with the analyzer's name. + if d.Category == "" { + d.Category = analyzer.Name + } else { + d.Category = analyzer.Name + "." + d.Category + } + rawDiagnostics = append(rawDiagnostics, d) + }, + ImportObjectFact: func(obj types.Object, ptr analysis.Fact) bool { + if obj == nil { + panic("nil object") + } + key := objectFactKey{obj, factType(ptr)} + + if v, ok := objectFacts[key]; ok { + reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem()) + return true + } + return false + }, + ExportObjectFact: func(obj types.Object, fact analysis.Fact) { + if obj.Pkg() != pkg.types { + panic(fmt.Sprintf("internal error: in analysis %s of package %s: Fact.Set(%s, %T): can't set facts on objects belonging another package", + analyzer, pkg.ID(), obj, fact)) + } + key := objectFactKey{obj, factType(fact)} + objectFacts[key] = fact // clobber any existing entry + }, + ImportPackageFact: func(pkg *types.Package, ptr analysis.Fact) bool { + if pkg == nil { + panic("nil package") + } + key := packageFactKey{pkg, factType(ptr)} + if v, ok := packageFacts[key]; ok { + reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem()) + return true + } + return false + }, + ExportPackageFact: func(fact analysis.Fact) { + key := packageFactKey{pkg.types, factType(fact)} + packageFacts[key] = fact // clobber any existing entry + }, + AllObjectFacts: func() []analysis.ObjectFact { + facts := make([]analysis.ObjectFact, 0, len(objectFacts)) + for k := range objectFacts { + facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: objectFacts[k]}) + } + return facts + }, + AllPackageFacts: func() []analysis.PackageFact { + facts := make([]analysis.PackageFact, 0, len(packageFacts)) + for k := range packageFacts { + facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: packageFacts[k]}) + } + return facts + }, + } + analysisinternal.SetTypeErrors(pass, pkg.typeErrors) + + if (pkg.HasListOrParseErrors() || pkg.HasTypeErrors()) && !analyzer.RunDespiteErrors { + return nil, fmt.Errorf("skipping analysis %s because package %s contains errors", analyzer.Name, pkg.ID()) + } + + // Recover from panics (only) within the analyzer logic. + // (Use an anonymous function to limit the recover scope.) + var result interface{} + var err error + func() { + defer func() { + if r := recover(); r != nil { + // An Analyzer crashed. This is often merely a symptom + // of a problem in package loading. + // + // We believe that CL 420538 may have fixed these crashes, so enable + // strict checks in tests. + const strict = true + if strict && bug.PanicOnBugs && analyzer.Name != "fact_purity" { + // During testing, crash. See issues 54762, 56035. + // But ignore analyzers with known crash bugs: + // - fact_purity (dominikh/go-tools#1327) + debug.SetTraceback("all") // show all goroutines + panic(r) + } else { + // In production, suppress the panic and press on. + err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) + } + } + }() + result, err = pass.Analyzer.Run(pass) + }() + if err != nil { + return nil, err + } + + if got, want := reflect.TypeOf(result), pass.Analyzer.ResultType; got != want { + return nil, fmt.Errorf( + "internal error: on package %s, analyzer %s returned a result of type %v, but declared ResultType %v", + pass.Pkg.Path(), pass.Analyzer, got, want) + } + + // disallow calls after Run + pass.ExportObjectFact = func(obj types.Object, fact analysis.Fact) { + panic(fmt.Sprintf("%s:%s: Pass.ExportObjectFact(%s, %T) called after Run", analyzer.Name, pkg.PkgPath(), obj, fact)) + } + pass.ExportPackageFact = func(fact analysis.Fact) { + panic(fmt.Sprintf("%s:%s: Pass.ExportPackageFact(%T) called after Run", analyzer.Name, pkg.PkgPath(), fact)) + } + + // Filter out facts related to objects that are irrelevant downstream + // (equivalently: not in the compiler export data). + for key := range objectFacts { + if !exportedFrom(key.obj, pkg.types) { + delete(objectFacts, key) + } + } + // TODO: filter out facts that belong to packages not + // mentioned in the export data to prevent side channels. + + var diagnostics []*source.Diagnostic + for _, diag := range rawDiagnostics { + srcDiags, err := analysisDiagnosticDiagnostics(snapshot, pkg, analyzer, &diag) + if err != nil { + event.Error(ctx, "unable to compute analysis error position", err, tag.Category.Of(diag.Category), tag.Package.Of(pkg.ID())) + continue + } + diagnostics = append(diagnostics, srcDiags...) + } + return &actionData{ + analyzer: analyzer, + pkgTypes: pkg.types, + diagnostics: diagnostics, + result: result, + objectFacts: objectFacts, + packageFacts: packageFacts, + }, nil +} + +// exportedFrom reports whether obj may be visible to a package that imports pkg. +// This includes not just the exported members of pkg, but also unexported +// constants, types, fields, and methods, perhaps belonging to other packages, +// that find there way into the API. +// This is an overapproximation of the more accurate approach used by +// gc export data, which walks the type graph, but it's much simpler. +// +// TODO(adonovan): do more accurate filtering by walking the type graph. +func exportedFrom(obj types.Object, pkg *types.Package) bool { + switch obj := obj.(type) { + case *types.Func: + return obj.Exported() && obj.Pkg() == pkg || + obj.Type().(*types.Signature).Recv() != nil + case *types.Var: + return obj.Exported() && obj.Pkg() == pkg || + obj.IsField() + case *types.TypeName: + return true + case *types.Const: + return obj.Exported() && obj.Pkg() == pkg + } + return false // Nil, Builtin, Label, or PkgName +} + +func factType(fact analysis.Fact) reflect.Type { + t := reflect.TypeOf(fact) + if t.Kind() != reflect.Ptr { + panic(fmt.Sprintf("invalid Fact type: got %T, want pointer", fact)) + } + return t +} + +func (s *snapshot) DiagnosePackage(ctx context.Context, spkg source.Package) (map[span.URI][]*source.Diagnostic, error) { + pkg := spkg.(*pkg) + var errorAnalyzerDiag []*source.Diagnostic + if pkg.HasTypeErrors() { + // Apply type error analyzers. + // They augment type error diagnostics with their own fixes. + var analyzers []*source.Analyzer + for _, a := range s.View().Options().TypeErrorAnalyzers { + analyzers = append(analyzers, a) + } + var err error + errorAnalyzerDiag, err = s.Analyze(ctx, pkg.ID(), analyzers) + if err != nil { + // Keep going: analysis failures should not block diagnostics. + event.Error(ctx, "type error analysis failed", err, tag.Package.Of(pkg.ID())) + } + } + diags := map[span.URI][]*source.Diagnostic{} + for _, diag := range pkg.diagnostics { + for _, eaDiag := range errorAnalyzerDiag { + if eaDiag.URI == diag.URI && eaDiag.Range == diag.Range && eaDiag.Message == diag.Message { + // Type error analyzers just add fixes and tags. Make a copy, + // since we don't own either, and overwrite. + // The analyzer itself can't do this merge because + // analysis.Diagnostic doesn't have all the fields, and Analyze + // can't because it doesn't have the type error, notably its code. + clone := *diag + clone.SuggestedFixes = eaDiag.SuggestedFixes + clone.Tags = eaDiag.Tags + clone.Analyzer = eaDiag.Analyzer + diag = &clone + } + } + diags[diag.URI] = append(diags[diag.URI], diag) + } + return diags, nil +} diff --git a/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go similarity index 81% rename from internal/lsp/cache/cache.go rename to gopls/internal/lsp/cache/cache.go index ac670b573e5..0eb00f23201 100644 --- a/internal/lsp/cache/cache.go +++ b/gopls/internal/lsp/cache/cache.go @@ -6,7 +6,6 @@ package cache import ( "context" - "crypto/sha256" "fmt" "go/ast" "go/token" @@ -21,31 +20,54 @@ import ( "sync/atomic" "time" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) -func New(options func(*source.Options)) *Cache { +// New Creates a new cache for gopls operation results, using the given file +// set, shared store, and session options. +// +// All of the fset, store and options may be nil, but if store is non-nil so +// must be fset (and they must always be used together), otherwise it may be +// possible to get cached data referencing token.Pos values not mapped by the +// FileSet. +func New(fset *token.FileSet, store *memoize.Store, options func(*source.Options)) *Cache { index := atomic.AddInt64(&cacheIndex, 1) + + if store != nil && fset == nil { + panic("non-nil store with nil fset") + } + if fset == nil { + fset = token.NewFileSet() + } + if store == nil { + store = &memoize.Store{} + } + c := &Cache{ id: strconv.FormatInt(index, 10), - fset: token.NewFileSet(), + fset: fset, options: options, + store: store, fileContent: map[span.URI]*fileHandle{}, } return c } type Cache struct { - id string - fset *token.FileSet + id string + fset *token.FileSet + + // TODO(rfindley): it doesn't make sense that cache accepts LSP options, just + // so that it can create a session: the cache does not (and should not) + // depend on options. Invert this relationship to remove options from Cache. options func(*source.Options) - store memoize.Store + store *memoize.Store fileMu sync.Mutex fileContent map[span.URI]*fileHandle @@ -55,7 +77,7 @@ type fileHandle struct { modTime time.Time uri span.URI bytes []byte - hash string + hash source.Hash err error // size is the file length as reported by Stat, for the purpose of @@ -69,6 +91,7 @@ func (h *fileHandle) Saved() bool { return true } +// GetFile stats and (maybe) reads the file, updates the cache, and returns it. func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { return c.getFile(ctx, uri) } @@ -101,7 +124,7 @@ func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error) return fh, nil } - fh, err := readFile(ctx, uri, fi) + fh, err := readFile(ctx, uri, fi) // ~25us if err != nil { return nil, err } @@ -126,7 +149,7 @@ func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, e _ = ctx defer done() - data, err := ioutil.ReadFile(uri.Filename()) + data, err := ioutil.ReadFile(uri.Filename()) // ~20us if err != nil { return &fileHandle{ modTime: fi.ModTime(), @@ -139,7 +162,7 @@ func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, e size: fi.Size(), uri: uri, bytes: data, - hash: hashContents(data), + hash: source.HashOf(data), }, nil } @@ -168,10 +191,6 @@ func (h *fileHandle) URI() span.URI { return h.uri } -func (h *fileHandle) Hash() string { - return h.hash -} - func (h *fileHandle) FileIdentity() source.FileIdentity { return source.FileIdentity{ URI: h.uri, @@ -183,10 +202,6 @@ func (h *fileHandle) Read() ([]byte, error) { return h.bytes, h.err } -func hashContents(contents []byte) string { - return fmt.Sprintf("%x", sha256.Sum256(contents)) -} - var cacheIndex, sessionIndex, viewIndex int64 func (c *Cache) ID() string { return c.id } @@ -207,17 +222,12 @@ func (c *Cache) PackageStats(withNames bool) template.HTML { c.store.DebugOnlyIterate(func(k, v interface{}) { switch k.(type) { case packageHandleKey: - v := v.(*packageData) + v := v.(typeCheckResult) if v.pkg == nil { break } - var typsCost, typInfoCost int64 - if v.pkg.types != nil { - typsCost = typesCost(v.pkg.types.Scope()) - } - if v.pkg.typesInfo != nil { - typInfoCost = typesInfoCost(v.pkg.typesInfo) - } + typsCost := typesCost(v.pkg.types.Scope()) + typInfoCost := typesInfoCost(v.pkg.typesInfo) stat := packageStat{ id: v.pkg.m.ID, mode: v.pkg.mode, diff --git a/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go similarity index 60% rename from internal/lsp/cache/check.go rename to gopls/internal/lsp/cache/check.go index b8a3655a9d4..17943efb707 100644 --- a/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -11,166 +11,96 @@ import ( "fmt" "go/ast" "go/types" - "path" "path/filepath" "regexp" - "sort" "strings" "sync" "golang.org/x/mod/module" + "golang.org/x/sync/errgroup" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" ) -type packageHandleKey string - -type packageHandle struct { - handle *memoize.Handle +// A packageKey identifies a packageHandle in the snapshot.packages map. +type packageKey struct { + mode source.ParseMode + id PackageID +} - goFiles, compiledGoFiles []*parseGoHandle +type packageHandleKey source.Hash - // mode is the mode the files were parsed in. - mode source.ParseMode +// A packageHandle is a handle to the future result of type-checking a package. +// The resulting package is obtained from the await() method. +type packageHandle struct { + promise *memoize.Promise // [typeCheckResult] // m is the metadata associated with the package. m *KnownMetadata // key is the hashed key for the package. + // + // It includes the all bits of the transitive closure of + // dependencies's sources. This is more than type checking + // really depends on: export data of direct deps should be + // enough. (The key for analysis actions could similarly + // hash only Facts of direct dependencies.) key packageHandleKey } -func (ph *packageHandle) packageKey() packageKey { - return packageKey{ - id: ph.m.ID, - mode: ph.mode, - } -} - -func (ph *packageHandle) imports(ctx context.Context, s source.Snapshot) (result []string) { - for _, pgh := range ph.goFiles { - f, err := s.ParseGo(ctx, pgh.file, source.ParseHeader) - if err != nil { - continue - } - seen := map[string]struct{}{} - for _, impSpec := range f.File.Imports { - imp := strings.Trim(impSpec.Path.Value, `"`) - if _, ok := seen[imp]; !ok { - seen[imp] = struct{}{} - result = append(result, imp) - } - } - } - - sort.Strings(result) - return result -} - -// packageData contains the data produced by type-checking a package. -type packageData struct { +// typeCheckResult contains the result of a call to +// typeCheckImpl, which type-checks a package. +type typeCheckResult struct { pkg *pkg err error } -// buildPackageHandle returns a packageHandle for a given package and mode. +// buildPackageHandle returns a handle for the future results of +// type-checking the package identified by id in the given mode. // It assumes that the given ID already has metadata available, so it does not // attempt to reload missing or invalid metadata. The caller must reload // metadata if needed. func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode source.ParseMode) (*packageHandle, error) { - if ph := s.getPackage(id, mode); ph != nil { - return ph, nil - } - - // Build the packageHandle for this ID and its dependencies. - ph, deps, err := s.buildKey(ctx, id, mode) - if err != nil { - return nil, err - } - - // Do not close over the packageHandle or the snapshot in the Bind function. - // This creates a cycle, which causes the finalizers to never run on the handles. - // The possible cycles are: - // - // packageHandle.h.function -> packageHandle - // packageHandle.h.function -> snapshot -> packageHandle - // - - m := ph.m - key := ph.key - - h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { - snapshot := arg.(*snapshot) - - // Begin loading the direct dependencies, in parallel. - var wg sync.WaitGroup - for _, dep := range deps { - wg.Add(1) - go func(dep *packageHandle) { - dep.check(ctx, snapshot) - wg.Done() - }(dep) - } - - data := &packageData{} - data.pkg, data.err = typeCheck(ctx, snapshot, m.Metadata, mode, deps) - // Make sure that the workers above have finished before we return, - // especially in case of cancellation. - wg.Wait() + packageKey := packageKey{id: id, mode: mode} - return data - }, nil) - ph.handle = h - - // Cache the handle in the snapshot. If a package handle has already - // been cached, addPackage will return the cached value. This is fine, - // since the original package handle above will have no references and be - // garbage collected. - ph = s.addPackageHandle(ph) - - return ph, nil -} + s.mu.Lock() + entry, hit := s.packages.Get(packageKey) + m := s.meta.metadata[id] + s.mu.Unlock() -// buildKey computes the key for a given packageHandle. -func (s *snapshot) buildKey(ctx context.Context, id PackageID, mode source.ParseMode) (*packageHandle, map[PackagePath]*packageHandle, error) { - m := s.getMetadata(id) if m == nil { - return nil, nil, fmt.Errorf("no metadata for %s", id) - } - goFiles, err := s.parseGoHandles(ctx, m.GoFiles, mode) - if err != nil { - return nil, nil, err - } - compiledGoFiles, err := s.parseGoHandles(ctx, m.CompiledGoFiles, mode) - if err != nil { - return nil, nil, err + return nil, fmt.Errorf("no metadata for %s", id) } - ph := &packageHandle{ - m: m, - goFiles: goFiles, - compiledGoFiles: compiledGoFiles, - mode: mode, - } - // Make sure all of the depList are sorted. - depList := append([]PackageID{}, m.Deps...) - sort.Slice(depList, func(i, j int) bool { - return depList[i] < depList[j] - }) - deps := make(map[PackagePath]*packageHandle) + if hit { + return entry.(*packageHandle), nil + } // Begin computing the key by getting the depKeys for all dependencies. - var depKeys []packageHandleKey - for _, depID := range depList { + // This requires reading the transitive closure of dependencies' source files. + // + // It is tempting to parallelize the recursion here, but + // without de-duplication of subtasks this would lead to an + // exponential amount of work, and computing the key is + // expensive as it reads all the source files transitively. + // Notably, we don't update the s.packages cache until the + // entire key has been computed. + // TODO(adonovan): use a promise cache to ensure that the key + // for each package is computed by at most one thread, then do + // the recursive key building of dependencies in parallel. + deps := make(map[PackageID]*packageHandle) + var depKey source.Hash // XOR of all unique deps + for _, depID := range m.DepsByPkgPath { depHandle, err := s.buildPackageHandle(ctx, depID, s.workspaceParseMode(depID)) // Don't use invalid metadata for dependencies if the top-level // metadata is valid. We only load top-level packages, so if the @@ -182,20 +112,98 @@ func (s *snapshot) buildKey(ctx context.Context, id PackageID, mode source.Parse event.Log(ctx, fmt.Sprintf("%s: invalid dep handle for %s", id, depID), tag.Snapshot.Of(s.id)) } + // This check ensures we break out of the slow + // buildPackageHandle recursion quickly when + // context cancelation is detected within GetFile. if ctx.Err() != nil { - return nil, nil, ctx.Err() + return nil, ctx.Err() // cancelled } - // One bad dependency should not prevent us from checking the entire package. - // Add a special key to mark a bad dependency. - depKeys = append(depKeys, packageHandleKey(fmt.Sprintf("%s import not found", depID))) + + // One bad dependency should not prevent us from + // checking the entire package. Leave depKeys[i] unset. continue } - deps[depHandle.m.PkgPath] = depHandle - depKeys = append(depKeys, depHandle.key) + + depKey.XORWith(source.Hash(depHandle.key)) + + deps[depID] = depHandle } + + // Read both lists of files of this package, in parallel. + // + // goFiles aren't presented to the type checker--nor + // are they included in the key, unsoundly--but their + // syntax trees are available from (*pkg).File(URI). + // TODO(adonovan): consider parsing them on demand? + // The need should be rare. + goFiles, compiledGoFiles, err := readGoFiles(ctx, s, m.Metadata) + if err != nil { + return nil, err + } + + // All the file reading has now been done. + // Create a handle for the result of type checking. experimentalKey := s.View().Options().ExperimentalPackageCacheKey - ph.key = checkPackageKey(ph.m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey) - return ph, deps, nil + phKey := computePackageKey(m.ID, compiledGoFiles, m, depKey, mode, experimentalKey) + promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} { + pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m.Metadata, mode, deps) + return typeCheckResult{pkg, err} + }) + + ph := &packageHandle{ + promise: promise, + m: m, + key: phKey, + } + + s.mu.Lock() + defer s.mu.Unlock() + + // Check that the metadata has not changed + // (which should invalidate this handle). + // + // (In future, handles should form a graph with edges from a + // packageHandle to the handles for parsing its files and the + // handles for type-checking its immediate deps, at which + // point there will be no need to even access s.meta.) + if s.meta.metadata[ph.m.ID].Metadata != ph.m.Metadata { + return nil, fmt.Errorf("stale metadata for %s", ph.m.ID) + } + + // Check cache again in case another thread got there first. + if prev, ok := s.packages.Get(packageKey); ok { + prevPH := prev.(*packageHandle) + release() + if prevPH.m.Metadata != ph.m.Metadata { + return nil, bug.Errorf("existing package handle does not match for %s", ph.m.ID) + } + return prevPH, nil + } + + // Update the map. + s.packages.Set(packageKey, ph, func(_, _ interface{}) { release() }) + + return ph, nil +} + +// readGoFiles reads the content of Metadata.GoFiles and +// Metadata.CompiledGoFiles, in parallel. +func readGoFiles(ctx context.Context, s *snapshot, m *Metadata) (goFiles, compiledGoFiles []source.FileHandle, err error) { + var group errgroup.Group + getFileHandles := func(files []span.URI) []source.FileHandle { + fhs := make([]source.FileHandle, len(files)) + for i, uri := range files { + i, uri := i, uri + group.Go(func() (err error) { + fhs[i], err = s.GetFile(ctx, uri) // ~25us + return + }) + } + return fhs + } + return getFileHandles(m.GoFiles), + getFileHandles(m.CompiledGoFiles), + group.Wait() } func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { @@ -208,13 +216,18 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode { if s.view.Options().MemoryMode == source.ModeNormal { return source.ParseFull } - if s.isActiveLocked(id, nil) { + if s.isActiveLocked(id) { return source.ParseFull } return source.ParseExported } -func checkPackageKey(id PackageID, pghs []*parseGoHandle, m *KnownMetadata, deps []packageHandleKey, mode source.ParseMode, experimentalKey bool) packageHandleKey { +// computePackageKey returns a key representing the act of type checking +// a package named id containing the specified files, metadata, and +// combined dependency hash. +func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey { + // TODO(adonovan): opt: no need to materalize the bytes; hash them directly. + // Also, use field separators to avoid spurious collisions. b := bytes.NewBuffer(nil) b.WriteString(string(id)) if m.Module != nil { @@ -225,38 +238,36 @@ func checkPackageKey(id PackageID, pghs []*parseGoHandle, m *KnownMetadata, deps // files, and deps). It should not otherwise affect the inputs to the type // checker, so this experiment omits it. This should increase cache hits on // the daemon as cfg contains the environment and working directory. - b.WriteString(hashConfig(m.Config)) + hc := hashConfig(m.Config) + b.Write(hc[:]) } b.WriteByte(byte(mode)) - for _, dep := range deps { - b.WriteString(string(dep)) + b.Write(depsKey[:]) + for _, file := range files { + b.WriteString(file.FileIdentity().String()) } - for _, cgf := range pghs { - b.WriteString(cgf.file.FileIdentity().String()) - } - return packageHandleKey(hashContents(b.Bytes())) -} - -// hashEnv returns a hash of the snapshot's configuration. -func hashEnv(s *snapshot) string { - s.view.optionsMu.Lock() - env := s.view.options.EnvSlice() - s.view.optionsMu.Unlock() - - b := &bytes.Buffer{} - for _, e := range env { - b.WriteString(e) - } - return hashContents(b.Bytes()) + // Metadata errors are interpreted and memoized on the computed package, so + // we must hash them into the key here. + // + // TODO(rfindley): handle metadata diagnostics independently from + // type-checking diagnostics. + for _, err := range m.Errors { + b.WriteString(err.Msg) + b.WriteString(err.Pos) + b.WriteRune(rune(err.Kind)) + } + return packageHandleKey(source.HashOf(b.Bytes())) } // hashConfig returns the hash for the *packages.Config. -func hashConfig(config *packages.Config) string { - b := bytes.NewBuffer(nil) +func hashConfig(config *packages.Config) source.Hash { + // TODO(adonovan): opt: don't materialize the bytes; hash them directly. + // Also, use sound field separators to avoid collisions. + var b bytes.Buffer // Dir, Mode, Env, BuildFlags are the parts of the config that can change. b.WriteString(config.Dir) - b.WriteString(string(rune(config.Mode))) + b.WriteRune(rune(config.Mode)) for _, e := range config.Env { b.WriteString(e) @@ -264,19 +275,16 @@ func hashConfig(config *packages.Config) string { for _, f := range config.BuildFlags { b.WriteString(f) } - return hashContents(b.Bytes()) + return source.HashOf(b.Bytes()) } -func (ph *packageHandle) Check(ctx context.Context, s source.Snapshot) (source.Package, error) { - return ph.check(ctx, s.(*snapshot)) -} - -func (ph *packageHandle) check(ctx context.Context, s *snapshot) (*pkg, error) { - v, err := ph.handle.Get(ctx, s.generation, s) +// await waits for typeCheckImpl to complete and returns its result. +func (ph *packageHandle) await(ctx context.Context, s *snapshot) (*pkg, error) { + v, err := s.awaitPromise(ctx, ph.promise) if err != nil { return nil, err } - data := v.(*packageData) + data := v.(typeCheckResult) return data.pkg, data.err } @@ -288,33 +296,44 @@ func (ph *packageHandle) ID() string { return string(ph.m.ID) } -func (ph *packageHandle) cached(g *memoize.Generation) (*pkg, error) { - v := ph.handle.Cached(g) +func (ph *packageHandle) cached() (*pkg, error) { + v := ph.promise.Cached() if v == nil { return nil, fmt.Errorf("no cached type information for %s", ph.m.PkgPath) } - data := v.(*packageData) + data := v.(typeCheckResult) return data.pkg, data.err } -func (s *snapshot) parseGoHandles(ctx context.Context, files []span.URI, mode source.ParseMode) ([]*parseGoHandle, error) { - pghs := make([]*parseGoHandle, 0, len(files)) - for _, uri := range files { - fh, err := s.GetFile(ctx, uri) - if err != nil { - return nil, err - } - pghs = append(pghs, s.parseGoHandle(ctx, fh, mode)) - } - return pghs, nil -} +// typeCheckImpl type checks the parsed source files in compiledGoFiles. +// (The resulting pkg also holds the parsed but not type-checked goFiles.) +// deps holds the future results of type-checking the direct dependencies. +func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle) (*pkg, error) { + // Start type checking of direct dependencies, + // in parallel and asynchronously. + // As the type checker imports each of these + // packages, it will wait for its completion. + var wg sync.WaitGroup + for _, dep := range deps { + wg.Add(1) + go func(dep *packageHandle) { + dep.await(ctx, snapshot) // ignore result + wg.Done() + }(dep) + } + // The 'defer' below is unusual but intentional: + // it is not necessary that each call to dep.check + // complete before type checking begins, as the type + // checker will wait for those it needs. But they do + // need to complete before this function returns and + // the snapshot is possibly destroyed. + defer wg.Wait() -func typeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle) (*pkg, error) { var filter *unexportedFilter if mode == source.ParseExported { filter = &unexportedFilter{uses: map[string]bool{}} } - pkg, err := doTypeCheck(ctx, snapshot, m, mode, deps, filter) + pkg, err := doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) if err != nil { return nil, err } @@ -325,16 +344,14 @@ func typeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source // time keeping those names. missing, unexpected := filter.ProcessErrors(pkg.typeErrors) if len(unexpected) == 0 && len(missing) != 0 { - event.Log(ctx, fmt.Sprintf("discovered missing identifiers: %v", missing), tag.Package.Of(string(m.ID))) - pkg, err = doTypeCheck(ctx, snapshot, m, mode, deps, filter) + pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, filter) if err != nil { return nil, err } missing, unexpected = filter.ProcessErrors(pkg.typeErrors) } if len(unexpected) != 0 || len(missing) != 0 { - event.Log(ctx, fmt.Sprintf("falling back to safe trimming due to type errors: %v or still-missing identifiers: %v", unexpected, missing), tag.Package.Of(string(m.ID))) - pkg, err = doTypeCheck(ctx, snapshot, m, mode, deps, nil) + pkg, err = doTypeCheck(ctx, snapshot, goFiles, compiledGoFiles, m, mode, deps, nil) if err != nil { return nil, err } @@ -426,15 +443,15 @@ func typeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) -func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { +func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle, astFilter *unexportedFilter) (*pkg, error) { ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(m.ID))) defer done() pkg := &pkg{ - m: m, - mode: mode, - imports: make(map[PackagePath]*pkg), - types: types.NewPackage(string(m.PkgPath), string(m.Name)), + m: m, + mode: mode, + deps: make(map[PackageID]*pkg), + types: types.NewPackage(string(m.PkgPath), string(m.Name)), typesInfo: &types.Info{ Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object), @@ -447,19 +464,19 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour } typeparams.InitInstanceInfo(pkg.typesInfo) - for _, gf := range pkg.m.GoFiles { - // In the presence of line directives, we may need to report errors in - // non-compiled Go files, so we need to register them on the package. - // However, we only need to really parse them in ParseFull mode, when - // the user might actually be looking at the file. - fh, err := snapshot.GetFile(ctx, gf) - if err != nil { - return nil, err - } - goMode := source.ParseFull - if mode != source.ParseFull { - goMode = source.ParseHeader - } + // In the presence of line directives, we may need to report errors in + // non-compiled Go files, so we need to register them on the package. + // However, we only need to really parse them in ParseFull mode, when + // the user might actually be looking at the file. + goMode := source.ParseFull + if mode != source.ParseFull { + goMode = source.ParseHeader + } + + // Parse the GoFiles. (These aren't presented to the type + // checker but are part of the returned pkg.) + // TODO(adonovan): opt: parallelize parsing. + for _, fh := range goFiles { pgf, err := snapshot.ParseGo(ctx, fh, goMode) if err != nil { return nil, err @@ -467,7 +484,8 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour pkg.goFiles = append(pkg.goFiles, pgf) } - if err := parseCompiledGoFiles(ctx, snapshot, mode, pkg, astFilter); err != nil { + // Parse the CompiledGoFiles: those seen by the compiler/typechecker. + if err := parseCompiledGoFiles(ctx, compiledGoFiles, snapshot, mode, pkg, astFilter); err != nil { return nil, err } @@ -501,23 +519,30 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour Error: func(e error) { pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) }, - Importer: importerFunc(func(pkgPath string) (*types.Package, error) { - // If the context was cancelled, we should abort. - if ctx.Err() != nil { - return nil, ctx.Err() + Importer: importerFunc(func(path string) (*types.Package, error) { + // While all of the import errors could be reported + // based on the metadata before we start type checking, + // reporting them via types.Importer places the errors + // at the correct source location. + id, ok := pkg.m.DepsByImpPath[ImportPath(path)] + if !ok { + // If the import declaration is broken, + // go list may fail to report metadata about it. + // See TestFixImportDecl for an example. + return nil, fmt.Errorf("missing metadata for import of %q", path) } - dep := resolveImportPath(pkgPath, pkg, deps) - if dep == nil { - return nil, snapshot.missingPkgError(ctx, pkgPath) + dep, ok := deps[id] // id may be "" + if !ok { + return nil, snapshot.missingPkgError(path) } if !source.IsValidImport(string(m.PkgPath), string(dep.m.PkgPath)) { - return nil, fmt.Errorf("invalid use of internal package %s", pkgPath) + return nil, fmt.Errorf("invalid use of internal package %s", path) } - depPkg, err := dep.check(ctx, snapshot) + depPkg, err := dep.await(ctx, snapshot) if err != nil { return nil, err } - pkg.imports[depPkg.m.PkgPath] = depPkg + pkg.deps[depPkg.m.ID] = depPkg return depPkg.types, nil }), } @@ -548,7 +573,7 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour } // Type checking errors are handled via the config, so ignore them here. - _ = check.Files(files) + _ = check.Files(files) // 50us-15ms, depending on size of package // If the context was cancelled, we may have returned a ton of transient // errors to the type checker. Swallow them. @@ -558,22 +583,17 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, m *Metadata, mode sour return pkg, nil } -func parseCompiledGoFiles(ctx context.Context, snapshot *snapshot, mode source.ParseMode, pkg *pkg, astFilter *unexportedFilter) error { - for _, cgf := range pkg.m.CompiledGoFiles { - fh, err := snapshot.GetFile(ctx, cgf) - if err != nil { - return err - } - +func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHandle, snapshot *snapshot, mode source.ParseMode, pkg *pkg, astFilter *unexportedFilter) error { + // TODO(adonovan): opt: parallelize this loop, which takes 1-25ms. + for _, fh := range compiledGoFiles { var pgf *source.ParsedGoFile - var fixed bool + var err error // Only parse Full through the cache -- we need to own Exported ASTs // to prune them. if mode == source.ParseFull { - pgf, fixed, err = snapshot.parseGo(ctx, fh, mode) + pgf, err = snapshot.ParseGo(ctx, fh, mode) } else { - d := parseGo(ctx, snapshot.FileSet(), fh, mode) - pgf, fixed, err = d.parsed, d.fixed, d.err + pgf, err = parseGoImpl(ctx, snapshot.FileSet(), fh, mode) // ~20us/KB } if err != nil { return err @@ -584,22 +604,26 @@ func parseCompiledGoFiles(ctx context.Context, snapshot *snapshot, mode source.P } // If we have fixed parse errors in any of the files, we should hide type // errors, as they may be completely nonsensical. - pkg.hasFixedFiles = pkg.hasFixedFiles || fixed - } - if mode != source.ParseExported { - return nil + pkg.hasFixedFiles = pkg.hasFixedFiles || pgf.Fixed } - if astFilter != nil { - var files []*ast.File - for _, cgf := range pkg.compiledGoFiles { - files = append(files, cgf.File) - } - astFilter.Filter(files) - } else { - for _, cgf := range pkg.compiledGoFiles { - trimAST(cgf.File) + + // Optionally remove parts that don't affect the exported API. + if mode == source.ParseExported { + if astFilter != nil { + // aggressive pruning based on reachability + var files []*ast.File + for _, cgf := range pkg.compiledGoFiles { + files = append(files, cgf.File) + } + astFilter.Filter(files) + } else { + // simple trimming of function bodies + for _, cgf := range pkg.compiledGoFiles { + trimAST(cgf.File) + } } } + return nil } @@ -659,7 +683,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost } for _, imp := range allImports[item] { - rng, err := source.NewMappedRange(s.FileSet(), imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range() + rng, err := source.NewMappedRange(imp.cgf.Tok, imp.cgf.Mapper, imp.imp.Pos(), imp.imp.End()).Range() if err != nil { return nil, err } @@ -709,7 +733,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost if reference == nil { continue } - rng, err := rangeFromPositions(pm.Mapper, reference.Start, reference.End) + rng, err := pm.Mapper.OffsetRange(reference.Start.Byte, reference.End.Byte) if err != nil { return nil, err } @@ -733,21 +757,19 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *pkg) ([]*source.Diagnost // missingPkgError returns an error message for a missing package that varies // based on the user's workspace mode. -func (s *snapshot) missingPkgError(ctx context.Context, pkgPath string) error { +func (s *snapshot) missingPkgError(pkgPath string) error { var b strings.Builder if s.workspaceMode()&moduleMode == 0 { gorootSrcPkg := filepath.FromSlash(filepath.Join(s.view.goroot, "src", pkgPath)) - - b.WriteString(fmt.Sprintf("cannot find package %q in any of \n\t%s (from $GOROOT)", pkgPath, gorootSrcPkg)) - + fmt.Fprintf(&b, "cannot find package %q in any of \n\t%s (from $GOROOT)", pkgPath, gorootSrcPkg) for _, gopath := range filepath.SplitList(s.view.gopath) { gopathSrcPkg := filepath.FromSlash(filepath.Join(gopath, "src", pkgPath)) - b.WriteString(fmt.Sprintf("\n\t%s (from $GOPATH)", gopathSrcPkg)) + fmt.Fprintf(&b, "\n\t%s (from $GOPATH)", gopathSrcPkg) } } else { - b.WriteString(fmt.Sprintf("no required module provides package %q", pkgPath)) - if err := s.getInitializationError(ctx); err != nil { - b.WriteString(fmt.Sprintf("(workspace configuration error: %s)", err.MainError)) + fmt.Fprintf(&b, "no required module provides package %q", pkgPath) + if err := s.getInitializationError(); err != nil { + fmt.Fprintf(&b, "\n(workspace configuration error: %s)", err.MainError) } } return errors.New(b.String()) @@ -820,41 +842,6 @@ func expandErrors(errs []types.Error, supportsRelatedInformation bool) []extende return result } -// resolveImportPath resolves an import path in pkg to a package from deps. -// It should produce the same results as resolveImportPath: -// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/load/pkg.go;drc=641918ee09cb44d282a30ee8b66f99a0b63eaef9;l=990. -func resolveImportPath(importPath string, pkg *pkg, deps map[PackagePath]*packageHandle) *packageHandle { - if dep := deps[PackagePath(importPath)]; dep != nil { - return dep - } - // We may be in GOPATH mode, in which case we need to check vendor dirs. - searchDir := path.Dir(pkg.PkgPath()) - for { - vdir := PackagePath(path.Join(searchDir, "vendor", importPath)) - if vdep := deps[vdir]; vdep != nil { - return vdep - } - - // Search until Dir doesn't take us anywhere new, e.g. "." or "/". - next := path.Dir(searchDir) - if searchDir == next { - break - } - searchDir = next - } - - // Vendor didn't work. Let's try minimal module compatibility mode. - // In MMC, the packagePath is the canonical (.../vN/...) path, which - // is hard to calculate. But the go command has already resolved the ID - // to the non-versioned path, and we can take advantage of that. - for _, dep := range deps { - if dep.ID() == importPath { - return dep - } - } - return nil -} - // An importFunc is an implementation of the single-method // types.Importer interface based on a function value. type importerFunc func(path string) (*types.Package, error) diff --git a/gopls/internal/lsp/cache/debug.go b/gopls/internal/lsp/cache/debug.go new file mode 100644 index 00000000000..d665b011daf --- /dev/null +++ b/gopls/internal/lsp/cache/debug.go @@ -0,0 +1,53 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "fmt" + "os" + "sort" +) + +// This file contains helpers that can be used to instrument code while +// debugging. + +// debugEnabled toggles the helpers below. +const debugEnabled = false + +// If debugEnabled is true, debugf formats its arguments and prints to stderr. +// If debugEnabled is false, it is a no-op. +func debugf(format string, args ...interface{}) { + if !debugEnabled { + return + } + if false { + _ = fmt.Sprintf(format, args...) // encourage vet to validate format strings + } + fmt.Fprintf(os.Stderr, ">>> "+format+"\n", args...) +} + +// If debugEnabled is true, dumpWorkspace prints a summary of workspace +// packages to stderr. If debugEnabled is false, it is a no-op. +func (s *snapshot) dumpWorkspace(context string) { + if !debugEnabled { + return + } + + debugf("workspace (after %s):", context) + var ids []PackageID + for id := range s.workspacePackages { + ids = append(ids, id) + } + + sort.Slice(ids, func(i, j int) bool { + return ids[i] < ids[j] + }) + + for _, id := range ids { + pkgPath := s.workspacePackages[id] + m, ok := s.meta.metadata[id] + debugf(" %s:%s (metadata: %t; valid: %t)", id, pkgPath, ok, m.Valid) + } +} diff --git a/internal/lsp/cache/error_test.go b/gopls/internal/lsp/cache/error_test.go similarity index 100% rename from internal/lsp/cache/error_test.go rename to gopls/internal/lsp/cache/error_test.go diff --git a/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go similarity index 85% rename from internal/lsp/cache/errors.go rename to gopls/internal/lsp/cache/errors.go index 342f2bea5d7..bca68d50491 100644 --- a/internal/lsp/cache/errors.go +++ b/gopls/internal/lsp/cache/errors.go @@ -15,11 +15,12 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/typesinternal" ) @@ -82,7 +83,7 @@ func parseErrorDiagnostics(snapshot *snapshot, pkg *pkg, errList scanner.ErrorLi return nil, err } pos := pgf.Tok.Pos(e.Pos.Offset) - spn, err := span.NewRange(snapshot.FileSet(), pos, pos).Span() + spn, err := span.NewRange(pgf.Tok, pos, pos).Span() if err != nil { return nil, err } @@ -122,6 +123,10 @@ func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*sou diag.Code = code.String() diag.CodeHref = typesCodeHref(snapshot, code) } + switch code { + case typesinternal.UnusedVar, typesinternal.UnusedImport: + diag.Tags = append(diag.Tags, protocol.Unnecessary) + } for _, secondary := range e.secondaries { _, secondarySpan, err := typeErrorData(snapshot.FileSet(), pkg, secondary) @@ -196,8 +201,15 @@ func analysisDiagnosticDiagnostics(snapshot *snapshot, pkg *pkg, a *analysis.Ana break } } - - spn, err := span.NewRange(snapshot.FileSet(), e.Pos, e.End).Span() + tokFile := snapshot.FileSet().File(e.Pos) + if tokFile == nil { + return nil, bug.Errorf("no file for position of %q diagnostic", e.Category) + } + end := e.End + if !end.IsValid() { + end = e.Pos + } + spn, err := span.NewRange(tokFile, e.Pos, end).Span() if err != nil { return nil, err } @@ -282,7 +294,15 @@ func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnos for _, fix := range diag.SuggestedFixes { edits := make(map[span.URI][]protocol.TextEdit) for _, e := range fix.TextEdits { - spn, err := span.NewRange(snapshot.FileSet(), e.Pos, e.End).Span() + tokFile := snapshot.FileSet().File(e.Pos) + if tokFile == nil { + return nil, bug.Errorf("no file for edit position") + } + end := e.End + if !end.IsValid() { + end = e.Pos + } + spn, err := span.NewRange(tokFile, e.Pos, end).Span() if err != nil { return nil, err } @@ -310,7 +330,15 @@ func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnos func relatedInformation(pkg *pkg, fset *token.FileSet, diag *analysis.Diagnostic) ([]source.RelatedInformation, error) { var out []source.RelatedInformation for _, related := range diag.Related { - spn, err := span.NewRange(fset, related.Pos, related.End).Span() + tokFile := fset.File(related.Pos) + if tokFile == nil { + return nil, bug.Errorf("no file for %q diagnostic position", diag.Category) + } + end := related.End + if !end.IsValid() { + end = related.Pos + } + spn, err := span.NewRange(tokFile, related.Pos, end).Span() if err != nil { return nil, err } @@ -333,7 +361,20 @@ func typeErrorData(fset *token.FileSet, pkg *pkg, terr types.Error) (typesintern start, end = terr.Pos, terr.Pos ecode = 0 } + // go/types may return invalid positions in some cases, such as + // in errors on tokens missing from the syntax tree. + if !start.IsValid() { + return 0, span.Span{}, fmt.Errorf("type error (%q, code %d, go116=%t) without position", terr.Msg, ecode, ok) + } + // go/types errors retain their FileSet. + // Sanity-check that we're using the right one. + if fset != terr.Fset { + return 0, span.Span{}, bug.Errorf("wrong FileSet for type error") + } posn := fset.Position(start) + if !posn.IsValid() { + return 0, span.Span{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr) + } pgf, err := pkg.File(span.URIFromPath(posn.Filename)) if err != nil { return 0, span.Span{}, err @@ -397,7 +438,7 @@ func parseGoListImportCycleError(snapshot *snapshot, e packages.Error, pkg *pkg) // Search file imports for the import that is causing the import cycle. for _, imp := range cgf.File.Imports { if imp.Path.Value == circImp { - spn, err := span.NewRange(snapshot.FileSet(), imp.Pos(), imp.End()).Span() + spn, err := span.NewRange(cgf.Tok, imp.Pos(), imp.End()).Span() if err != nil { return msg, span.Span{}, false } diff --git a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go new file mode 100644 index 00000000000..1dac0767afb --- /dev/null +++ b/gopls/internal/lsp/cache/graph.go @@ -0,0 +1,156 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "sort" + + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" +) + +// A metadataGraph is an immutable and transitively closed import +// graph of Go packages, as obtained from go/packages. +type metadataGraph struct { + // metadata maps package IDs to their associated metadata. + metadata map[PackageID]*KnownMetadata + + // importedBy maps package IDs to the list of packages that import them. + importedBy map[PackageID][]PackageID + + // ids maps file URIs to package IDs, sorted by (!valid, cli, packageID). + // A single file may belong to multiple packages due to tests packages. + ids map[span.URI][]PackageID +} + +// Clone creates a new metadataGraph, applying the given updates to the +// receiver. +func (g *metadataGraph) Clone(updates map[PackageID]*KnownMetadata) *metadataGraph { + if len(updates) == 0 { + // Optimization: since the graph is immutable, we can return the receiver. + return g + } + result := &metadataGraph{metadata: make(map[PackageID]*KnownMetadata, len(g.metadata))} + // Copy metadata. + for id, m := range g.metadata { + result.metadata[id] = m + } + for id, m := range updates { + if m == nil { + delete(result.metadata, id) + } else { + result.metadata[id] = m + } + } + result.build() + return result +} + +// build constructs g.importedBy and g.uris from g.metadata. +func (g *metadataGraph) build() { + // Build the import graph. + g.importedBy = make(map[PackageID][]PackageID) + for id, m := range g.metadata { + for _, depID := range m.DepsByPkgPath { + g.importedBy[depID] = append(g.importedBy[depID], id) + } + } + + // Collect file associations. + g.ids = make(map[span.URI][]PackageID) + for id, m := range g.metadata { + uris := map[span.URI]struct{}{} + for _, uri := range m.CompiledGoFiles { + uris[uri] = struct{}{} + } + for _, uri := range m.GoFiles { + uris[uri] = struct{}{} + } + for uri := range uris { + g.ids[uri] = append(g.ids[uri], id) + } + } + + // Sort and filter file associations. + // + // We choose the first non-empty set of package associations out of the + // following. For simplicity, call a non-command-line-arguments package a + // "real" package. + // + // 1: valid real packages + // 2: a valid command-line-arguments package + // 3: invalid real packages + // 4: an invalid command-line-arguments package + for uri, ids := range g.ids { + sort.Slice(ids, func(i, j int) bool { + // 1. valid packages appear earlier. + validi := g.metadata[ids[i]].Valid + validj := g.metadata[ids[j]].Valid + if validi != validj { + return validi + } + + // 2. command-line-args packages appear later. + cli := source.IsCommandLineArguments(string(ids[i])) + clj := source.IsCommandLineArguments(string(ids[j])) + if cli != clj { + return clj + } + + // 3. packages appear in name order. + return ids[i] < ids[j] + }) + + // Choose the best IDs for each URI, according to the following rules: + // - If there are any valid real packages, choose them. + // - Else, choose the first valid command-line-argument package, if it exists. + // - Else, keep using all the invalid metadata. + // + // TODO(rfindley): it might be better to track all IDs here, and exclude + // them later in PackagesForFile, but this is the existing behavior. + hasValidMetadata := false + for i, id := range ids { + m := g.metadata[id] + if m.Valid { + hasValidMetadata = true + } else if hasValidMetadata { + g.ids[uri] = ids[:i] + break + } + // If we've seen *anything* prior to command-line arguments package, take + // it. Note that ids[0] may itself be command-line-arguments. + if i > 0 && source.IsCommandLineArguments(string(id)) { + g.ids[uri] = ids[:i] + break + } + } + } +} + +// reverseTransitiveClosure calculates the set of packages that transitively +// import an id in ids. The result also includes given ids. +// +// If includeInvalid is false, the algorithm ignores packages with invalid +// metadata (including those in the given list of ids). +func (g *metadataGraph) reverseTransitiveClosure(includeInvalid bool, ids ...PackageID) map[PackageID]bool { + seen := make(map[PackageID]bool) + var visitAll func([]PackageID) + visitAll = func(ids []PackageID) { + for _, id := range ids { + if seen[id] { + continue + } + m := g.metadata[id] + // Only use invalid metadata if we support it. + if m == nil || !(m.Valid || includeInvalid) { + continue + } + seen[id] = true + visitAll(g.importedBy[id]) + } + } + visitAll(ids) + return seen +} diff --git a/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go similarity index 68% rename from internal/lsp/cache/imports.go rename to gopls/internal/lsp/cache/imports.go index 01a2468ef34..2bda377746d 100644 --- a/internal/lsp/cache/imports.go +++ b/gopls/internal/lsp/cache/imports.go @@ -7,61 +7,48 @@ package cache import ( "context" "fmt" + "os" "reflect" "strings" "sync" "time" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/source" ) type importsState struct { ctx context.Context - mu sync.Mutex - processEnv *imports.ProcessEnv - cleanupProcessEnv func() - cacheRefreshDuration time.Duration - cacheRefreshTimer *time.Timer - cachedModFileHash string - cachedBuildFlags []string + mu sync.Mutex + processEnv *imports.ProcessEnv + cleanupProcessEnv func() + cacheRefreshDuration time.Duration + cacheRefreshTimer *time.Timer + cachedModFileHash source.Hash + cachedBuildFlags []string + cachedDirectoryFilters []string } func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error { s.mu.Lock() defer s.mu.Unlock() - // Find the hash of the active mod file, if any. Using the unsaved content + // Find the hash of active mod files, if any. Using the unsaved content // is slightly wasteful, since we'll drop caches a little too often, but // the mod file shouldn't be changing while people are autocompleting. - var modFileHash string - // If we are using 'legacyWorkspace' mode, we can just read the modfile from - // the snapshot. Otherwise, we need to get the synthetic workspace mod file. // - // TODO(rfindley): we should be able to just always use the synthetic - // workspace module, or alternatively use the go.work file. - if snapshot.workspace.moduleSource == legacyWorkspace { - for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element - modFH, err := snapshot.GetFile(ctx, m) - if err != nil { - return err - } - modFileHash = modFH.FileIdentity().Hash - } - } else { - modFile, err := snapshot.workspace.modFile(ctx, snapshot) - if err != nil { - return err - } - modBytes, err := modFile.Format() + // TODO(rfindley): consider instead hashing on-disk modfiles here. + var modFileHash source.Hash + for m := range snapshot.workspace.ActiveModFiles() { + fh, err := snapshot.GetFile(ctx, m) if err != nil { return err } - modFileHash = hashContents(modBytes) + modFileHash.XORWith(fh.FileIdentity().Hash) } // view.goEnv is immutable -- changes make a new view. Options can change. @@ -69,9 +56,11 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot snapshot.view.optionsMu.Lock() localPrefix := snapshot.view.options.Local currentBuildFlags := snapshot.view.options.BuildFlags + currentDirectoryFilters := snapshot.view.options.DirectoryFilters changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) || - modFileHash != s.cachedModFileHash + modFileHash != s.cachedModFileHash || + !reflect.DeepEqual(snapshot.view.options.DirectoryFilters, s.cachedDirectoryFilters) snapshot.view.optionsMu.Unlock() // If anything relevant to imports has changed, clear caches and @@ -91,6 +80,7 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot } s.cachedModFileHash = modFileHash s.cachedBuildFlags = currentBuildFlags + s.cachedDirectoryFilters = currentDirectoryFilters var err error s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot) if err != nil { @@ -141,20 +131,21 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho pe.Logf = nil } - // Take an extra reference to the snapshot so that its workspace directory - // (if any) isn't destroyed while we're using it. - release := snapshot.generation.Acquire() + // Extract invocation details from the snapshot to use with goimports. + // + // TODO(rfindley): refactor to extract the necessary invocation logic into + // separate functions. Using goCommandInvocation is unnecessarily indirect, + // and has led to memory leaks in the past, when the snapshot was + // unintentionally held past its lifetime. _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ WorkingDir: snapshot.view.rootURI.Filename(), }) if err != nil { return nil, err } - pe.WorkingDir = inv.WorkingDir + pe.BuildFlags = inv.BuildFlags - pe.WorkingDir = inv.WorkingDir - pe.ModFile = inv.ModFile - pe.ModFlag = inv.ModFlag + pe.ModFlag = "readonly" // processEnv operations should not mutate the modfile pe.Env = map[string]string{} for _, kv := range inv.Env { split := strings.SplitN(kv, "=", 2) @@ -163,11 +154,31 @@ func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapsho } pe.Env[split[0]] = split[1] } + // We don't actually use the invocation, so clean it up now. + cleanupInvocation() + + // If the snapshot uses a synthetic workspace directory, create a copy for + // the lifecycle of the importsState. + // + // Notably, we cannot use the snapshot invocation working directory, as that + // is tied to the lifecycle of the snapshot. + // + // Otherwise return a no-op cleanup function. + cleanup = func() {} + if snapshot.usesWorkspaceDir() { + tmpDir, err := makeWorkspaceDir(ctx, snapshot.workspace, snapshot) + if err != nil { + return nil, err + } + pe.WorkingDir = tmpDir + cleanup = func() { + os.RemoveAll(tmpDir) // ignore error + } + } else { + pe.WorkingDir = snapshot.view.rootURI.Filename() + } - return func() { - cleanupInvocation() - release() - }, nil + return cleanup, nil } func (s *importsState) refreshProcessEnv() { diff --git a/internal/lsp/cache/keys.go b/gopls/internal/lsp/cache/keys.go similarity index 100% rename from internal/lsp/cache/keys.go rename to gopls/internal/lsp/cache/keys.go diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go new file mode 100644 index 00000000000..9cabffc0d03 --- /dev/null +++ b/gopls/internal/lsp/cache/load.go @@ -0,0 +1,783 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "sync/atomic" + "time" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/gocommand" + "golang.org/x/tools/internal/packagesinternal" +) + +var loadID uint64 // atomic identifier for loads + +// errNoPackages indicates that a load query matched no packages. +var errNoPackages = errors.New("no packages returned") + +// load calls packages.Load for the given scopes, updating package metadata, +// import graph, and mapped files with the result. +// +// The resulting error may wrap the moduleErrorMap error type, representing +// errors associated with specific modules. +func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) { + id := atomic.AddUint64(&loadID, 1) + eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging + + var query []string + var containsDir bool // for logging + + // Keep track of module query -> module path so that we can later correlate query + // errors with errors. + moduleQueries := make(map[string]string) + for _, scope := range scopes { + switch scope := scope.(type) { + case packageLoadScope: + if source.IsCommandLineArguments(string(scope)) { + panic("attempted to load command-line-arguments") + } + // The only time we pass package paths is when we're doing a + // partial workspace load. In those cases, the paths came back from + // go list and should already be GOPATH-vendorized when appropriate. + query = append(query, string(scope)) + + case fileLoadScope: + uri := span.URI(scope) + fh := s.FindFile(uri) + if fh == nil || s.View().FileKind(fh) != source.Go { + // Don't try to load a file that doesn't exist, or isn't a go file. + continue + } + contents, err := fh.Read() + if err != nil { + continue + } + if isStandaloneFile(contents, s.view.Options().StandaloneTags) { + query = append(query, uri.Filename()) + } else { + query = append(query, fmt.Sprintf("file=%s", uri.Filename())) + } + + case moduleLoadScope: + switch scope { + case "std", "cmd": + query = append(query, string(scope)) + default: + modQuery := fmt.Sprintf("%s/...", scope) + query = append(query, modQuery) + moduleQueries[modQuery] = string(scope) + } + + case viewLoadScope: + // If we are outside of GOPATH, a module, or some other known + // build system, don't load subdirectories. + if !s.ValidBuildConfiguration() { + query = append(query, "./") + } else { + query = append(query, "./...") + } + + default: + panic(fmt.Sprintf("unknown scope type %T", scope)) + } + switch scope.(type) { + case viewLoadScope, moduleLoadScope: + containsDir = true + } + } + if len(query) == 0 { + return nil + } + sort.Strings(query) // for determinism + + if s.view.Options().VerboseWorkDoneProgress { + work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading query=%s", query), nil, nil) + defer work.End(ctx, "Done.") + } + + ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query)) + defer done() + + flags := source.LoadWorkspace + if allowNetwork { + flags |= source.AllowNetwork + } + _, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{ + WorkingDir: s.view.rootURI.Filename(), + }) + if err != nil { + return err + } + + // Set a last resort deadline on packages.Load since it calls the go + // command, which may hang indefinitely if it has a bug. golang/go#42132 + // and golang/go#42255 have more context. + ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) + defer cancel() + + cfg := s.config(ctx, inv) + pkgs, err := packages.Load(cfg, query...) + cleanup() + + // If the context was canceled, return early. Otherwise, we might be + // type-checking an incomplete result. Check the context directly, + // because go/packages adds extra information to the error. + if ctx.Err() != nil { + return ctx.Err() + } + + // This log message is sought for by TestReloadOnlyOnce. + if err != nil { + event.Error(ctx, eventName, err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) + } else { + event.Log(ctx, eventName, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) + } + + if len(pkgs) == 0 { + if err == nil { + err = errNoPackages + } + return fmt.Errorf("packages.Load error: %w", err) + } + + moduleErrs := make(map[string][]packages.Error) // module path -> errors + filterer := buildFilterer(s.view.rootURI.Filename(), s.view.gomodcache, s.view.Options()) + newMetadata := make(map[PackageID]*KnownMetadata) + for _, pkg := range pkgs { + // The Go command returns synthetic list results for module queries that + // encountered module errors. + // + // For example, given a module path a.mod, we'll query for "a.mod/..." and + // the go command will return a package named "a.mod/..." holding this + // error. Save it for later interpretation. + // + // See golang/go#50862 for more details. + if mod := moduleQueries[pkg.PkgPath]; mod != "" { // a synthetic result for the unloadable module + if len(pkg.Errors) > 0 { + moduleErrs[mod] = pkg.Errors + } + continue + } + + if !containsDir || s.view.Options().VerboseOutput { + event.Log(ctx, eventName, + tag.Snapshot.Of(s.ID()), + tag.Package.Of(pkg.ID), + tag.Files.Of(pkg.CompiledGoFiles)) + } + // Ignore packages with no sources, since we will never be able to + // correctly invalidate that metadata. + if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 { + continue + } + // Special case for the builtin package, as it has no dependencies. + if pkg.PkgPath == "builtin" { + if len(pkg.GoFiles) != 1 { + return fmt.Errorf("only expected 1 file for builtin, got %v", len(pkg.GoFiles)) + } + s.setBuiltin(pkg.GoFiles[0]) + continue + } + // Skip test main packages. + if isTestMain(pkg, s.view.gocache) { + continue + } + // Skip filtered packages. They may be added anyway if they're + // dependencies of non-filtered packages. + // + // TODO(rfindley): why exclude metadata arbitrarily here? It should be safe + // to capture all metadata. + if s.view.allFilesExcluded(pkg, filterer) { + continue + } + if err := buildMetadata(ctx, pkg, cfg, query, newMetadata, nil); err != nil { + return err + } + } + + s.mu.Lock() + + // Only update metadata where we don't already have valid metadata. + // + // We want to preserve an invariant that s.packages.Get(id).m.Metadata + // matches s.meta.metadata[id].Metadata. By avoiding overwriting valid + // metadata, we minimize the amount of invalidation required to preserve this + // invariant. + // + // TODO(rfindley): perform a sanity check that metadata matches here. If not, + // we have an invalidation bug elsewhere. + updates := make(map[PackageID]*KnownMetadata) + var updatedIDs []PackageID + for _, m := range newMetadata { + if existing := s.meta.metadata[m.ID]; existing == nil || !existing.Valid { + updates[m.ID] = m + updatedIDs = append(updatedIDs, m.ID) + delete(s.shouldLoad, m.ID) + } + } + + event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates))) + + // Invalidate the reverse transitive closure of packages that have changed. + // + // Note that the original metadata is being invalidated here, so we use the + // original metadata graph to compute the reverse closure. + invalidatedPackages := s.meta.reverseTransitiveClosure(true, updatedIDs...) + + s.meta = s.meta.Clone(updates) + s.resetIsActivePackageLocked() + + // Invalidate any packages and analysis results we may have associated with + // this metadata. + // + // Generally speaking we should have already invalidated these results in + // snapshot.clone, but with experimentalUseInvalidMetadata is may be possible + // that we have re-computed stale results before the reload completes. In + // this case, we must re-invalidate here. + // + // TODO(golang/go#54180): if we decide to make experimentalUseInvalidMetadata + // obsolete, we should avoid this invalidation. + s.invalidatePackagesLocked(invalidatedPackages) + + s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta) + s.dumpWorkspace("load") + s.mu.Unlock() + + // Recompute the workspace package handle for any packages we invalidated. + // + // This is (putatively) an optimization since handle + // construction prefetches the content of all Go source files. + // It is safe to ignore errors, or omit this step entirely. + for _, m := range updates { + s.buildPackageHandle(ctx, m.ID, s.workspaceParseMode(m.ID)) // ignore error + } + + if len(moduleErrs) > 0 { + return &moduleErrorMap{moduleErrs} + } + + return nil +} + +type moduleErrorMap struct { + errs map[string][]packages.Error // module path -> errors +} + +func (m *moduleErrorMap) Error() string { + var paths []string // sort for stability + for path, errs := range m.errs { + if len(errs) > 0 { // should always be true, but be cautious + paths = append(paths, path) + } + } + sort.Strings(paths) + + var buf bytes.Buffer + fmt.Fprintf(&buf, "%d modules have errors:\n", len(paths)) + for _, path := range paths { + fmt.Fprintf(&buf, "\t%s:%s\n", path, m.errs[path][0].Msg) + } + + return buf.String() +} + +// workspaceLayoutErrors returns a diagnostic for every open file, as well as +// an error message if there are no open files. +func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError { + // TODO(rfindley): do we really not want to show a critical error if the user + // has no go.mod files? + if len(s.workspace.getKnownModFiles()) == 0 { + return nil + } + + // TODO(rfindley): both of the checks below should be delegated to the workspace. + if s.view.userGo111Module == off { + return nil + } + if s.workspace.moduleSource != legacyWorkspace { + return nil + } + + // If the user has one module per view, there is nothing to warn about. + if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 { + return nil + } + + // Apply diagnostics about the workspace configuration to relevant open + // files. + openFiles := s.openFiles() + + // If the snapshot does not have a valid build configuration, it may be + // that the user has opened a directory that contains multiple modules. + // Check for that an warn about it. + if !s.ValidBuildConfiguration() { + var msg string + if s.view.goversion >= 18 { + msg = `gopls was not able to find modules in your workspace. +When outside of GOPATH, gopls needs to know which modules you are working on. +You can fix this by opening your workspace to a folder inside a Go module, or +by using a go.work file to specify multiple modules. +See the documentation for more information on setting up your workspace: +https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` + } else { + msg = `gopls requires a module at the root of your workspace. +You can work with multiple modules by upgrading to Go 1.18 or later, and using +go workspaces (go.work files). +See the documentation for more information on setting up your workspace: +https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` + } + return &source.CriticalError{ + MainError: fmt.Errorf(msg), + Diagnostics: s.applyCriticalErrorToFiles(ctx, msg, openFiles), + } + } + + // If the user has one active go.mod file, they may still be editing files + // in nested modules. Check the module of each open file and add warnings + // that the nested module must be opened as a workspace folder. + if len(s.workspace.ActiveModFiles()) == 1 { + // Get the active root go.mod file to compare against. + var rootModURI span.URI + for uri := range s.workspace.ActiveModFiles() { + rootModURI = uri + } + nestedModules := map[string][]source.VersionedFileHandle{} + for _, fh := range openFiles { + modURI := moduleForURI(s.workspace.knownModFiles, fh.URI()) + if modURI != rootModURI { + modDir := filepath.Dir(modURI.Filename()) + nestedModules[modDir] = append(nestedModules[modDir], fh) + } + } + // Add a diagnostic to each file in a nested module to mark it as + // "orphaned". Don't show a general diagnostic in the progress bar, + // because the user may still want to edit a file in a nested module. + var srcDiags []*source.Diagnostic + for modDir, uris := range nestedModules { + msg := fmt.Sprintf(`This file is in %s, which is a nested module in the %s module. +gopls currently requires one module per workspace folder. +Please open %s as a separate workspace folder. +You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md. +`, modDir, filepath.Dir(rootModURI.Filename()), modDir) + srcDiags = append(srcDiags, s.applyCriticalErrorToFiles(ctx, msg, uris)...) + } + if len(srcDiags) != 0 { + return &source.CriticalError{ + MainError: fmt.Errorf(`You are working in a nested module. +Please open it as a separate workspace folder. Learn more: +https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`), + Diagnostics: srcDiags, + } + } + } + return nil +} + +func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Diagnostic { + var srcDiags []*source.Diagnostic + for _, fh := range files { + // Place the diagnostics on the package or module declarations. + var rng protocol.Range + switch s.view.FileKind(fh) { + case source.Go: + if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil { + // Check that we have a valid `package foo` range to use for positioning the error. + if pgf.File.Package.IsValid() && pgf.File.Name != nil && pgf.File.Name.End().IsValid() { + pkgDecl := span.NewRange(pgf.Tok, pgf.File.Package, pgf.File.Name.End()) + if spn, err := pkgDecl.Span(); err == nil { + rng, _ = pgf.Mapper.Range(spn) + } + } + } + case source.Mod: + if pmf, err := s.ParseMod(ctx, fh); err == nil { + if mod := pmf.File.Module; mod != nil && mod.Syntax != nil { + rng, _ = pmf.Mapper.OffsetRange(mod.Syntax.Start.Byte, mod.Syntax.End.Byte) + } + } + } + srcDiags = append(srcDiags, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityError, + Source: source.ListError, + Message: msg, + }) + } + return srcDiags +} + +// getWorkspaceDir returns the URI for the workspace directory +// associated with this snapshot. The workspace directory is a +// temporary directory containing the go.mod file computed from all +// active modules. +func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) { + s.mu.Lock() + dir, err := s.workspaceDir, s.workspaceDirErr + s.mu.Unlock() + if dir == "" && err == nil { // cache miss + dir, err = makeWorkspaceDir(ctx, s.workspace, s) + s.mu.Lock() + s.workspaceDir, s.workspaceDirErr = dir, err + s.mu.Unlock() + } + return span.URIFromPath(dir), err +} + +// makeWorkspaceDir creates a temporary directory containing a go.mod +// and go.sum file for each module in the workspace. +// Note: snapshot's mutex must be unlocked for it to satisfy FileSource. +func makeWorkspaceDir(ctx context.Context, workspace *workspace, fs source.FileSource) (string, error) { + file, err := workspace.modFile(ctx, fs) + if err != nil { + return "", err + } + modContent, err := file.Format() + if err != nil { + return "", err + } + sumContent, err := workspace.sumFile(ctx, fs) + if err != nil { + return "", err + } + tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod") + if err != nil { + return "", err + } + for name, content := range map[string][]byte{ + "go.mod": modContent, + "go.sum": sumContent, + } { + if err := ioutil.WriteFile(filepath.Join(tmpdir, name), content, 0644); err != nil { + os.RemoveAll(tmpdir) // ignore error + return "", err + } + } + return tmpdir, nil +} + +// buildMetadata populates the updates map with metadata updates to +// apply, based on the given pkg. It recurs through pkg.Imports to ensure that +// metadata exists for all dependencies. +func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error { + // Allow for multiple ad-hoc packages in the workspace (see #47584). + pkgPath := PackagePath(pkg.PkgPath) + id := PackageID(pkg.ID) + if source.IsCommandLineArguments(pkg.ID) { + suffix := ":" + strings.Join(query, ",") + id = PackageID(string(id) + suffix) + pkgPath = PackagePath(string(pkgPath) + suffix) + } + + if _, ok := updates[id]; ok { + // If we've already seen this dependency, there may be an import cycle, or + // we may have reached the same package transitively via distinct paths. + // Check the path to confirm. + + // TODO(rfindley): this doesn't look sufficient. Any single piece of new + // metadata could theoretically introduce import cycles in the metadata + // graph. What's the point of this limited check here (and is it even + // possible to get an import cycle in data from go/packages)? Consider + // simply returning, so that this function need not return an error. + // + // We should consider doing a more complete guard against import cycles + // elsewhere. + for _, prev := range path { + if prev == id { + return fmt.Errorf("import cycle detected: %q", id) + } + } + return nil + } + + // Recreate the metadata rather than reusing it to avoid locking. + m := &KnownMetadata{ + Metadata: &Metadata{ + ID: id, + PkgPath: pkgPath, + Name: PackageName(pkg.Name), + ForTest: PackagePath(packagesinternal.GetForTest(pkg)), + TypesSizes: pkg.TypesSizes, + Config: cfg, + Module: pkg.Module, + depsErrors: packagesinternal.GetDepsErrors(pkg), + }, + Valid: true, + } + updates[id] = m + + for _, err := range pkg.Errors { + // Filter out parse errors from go list. We'll get them when we + // actually parse, and buggy overlay support may generate spurious + // errors. (See TestNewModule_Issue38207.) + if strings.Contains(err.Msg, "expected '") { + continue + } + m.Errors = append(m.Errors, err) + } + + for _, filename := range pkg.CompiledGoFiles { + uri := span.URIFromPath(filename) + m.CompiledGoFiles = append(m.CompiledGoFiles, uri) + } + for _, filename := range pkg.GoFiles { + uri := span.URIFromPath(filename) + m.GoFiles = append(m.GoFiles, uri) + } + + depsByImpPath := make(map[ImportPath]PackageID) + depsByPkgPath := make(map[PackagePath]PackageID) + for importPath, imported := range pkg.Imports { + importPath := ImportPath(importPath) + + // It is not an invariant that importPath == imported.PkgPath. + // For example, package "net" imports "golang.org/x/net/dns/dnsmessage" + // which refers to the package whose ID and PkgPath are both + // "vendor/golang.org/x/net/dns/dnsmessage". Notice the ImportMap, + // which maps ImportPaths to PackagePaths: + // + // $ go list -json net vendor/golang.org/x/net/dns/dnsmessage + // { + // "ImportPath": "net", + // "Name": "net", + // "Imports": [ + // "C", + // "vendor/golang.org/x/net/dns/dnsmessage", + // "vendor/golang.org/x/net/route", + // ... + // ], + // "ImportMap": { + // "golang.org/x/net/dns/dnsmessage": "vendor/golang.org/x/net/dns/dnsmessage", + // "golang.org/x/net/route": "vendor/golang.org/x/net/route" + // }, + // ... + // } + // { + // "ImportPath": "vendor/golang.org/x/net/dns/dnsmessage", + // "Name": "dnsmessage", + // ... + // } + // + // (Beware that, for historical reasons, go list uses + // the JSON field "ImportPath" for the package's + // path--effectively the linker symbol prefix.) + + // Don't remember any imports with significant errors. + // + // The len=0 condition is a heuristic check for imports of + // non-existent packages (for which go/packages will create + // an edge to a synthesized node). The heuristic is unsound + // because some valid packages have zero files, for example, + // a directory containing only the file p_test.go defines an + // empty package p. + // TODO(adonovan): clarify this. Perhaps go/packages should + // report which nodes were synthesized. + if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 { + depsByImpPath[importPath] = "" // missing + continue + } + + depsByImpPath[importPath] = PackageID(imported.ID) + depsByPkgPath[PackagePath(imported.PkgPath)] = PackageID(imported.ID) + if err := buildMetadata(ctx, imported, cfg, query, updates, append(path, id)); err != nil { + event.Error(ctx, "error in dependency", err) + } + } + m.DepsByImpPath = depsByImpPath + m.DepsByPkgPath = depsByPkgPath + + return nil +} + +// containsPackageLocked reports whether p is a workspace package for the +// snapshot s. +// +// s.mu must be held while calling this function. +func containsPackageLocked(s *snapshot, m *Metadata) bool { + // In legacy workspace mode, or if a package does not have an associated + // module, a package is considered inside the workspace if any of its files + // are under the workspace root (and not excluded). + // + // Otherwise if the package has a module it must be an active module (as + // defined by the module root or go.work file) and at least one file must not + // be filtered out by directoryFilters. + if m.Module != nil && s.workspace.moduleSource != legacyWorkspace { + modURI := span.URIFromPath(m.Module.GoMod) + _, ok := s.workspace.activeModFiles[modURI] + if !ok { + return false + } + + uris := map[span.URI]struct{}{} + for _, uri := range m.CompiledGoFiles { + uris[uri] = struct{}{} + } + for _, uri := range m.GoFiles { + uris[uri] = struct{}{} + } + + filterFunc := s.view.filterFunc() + for uri := range uris { + // Don't use view.contains here. go.work files may include modules + // outside of the workspace folder. + if !strings.Contains(string(uri), "/vendor/") && !filterFunc(uri) { + return true + } + } + return false + } + + return containsFileInWorkspaceLocked(s, m) +} + +// containsOpenFileLocked reports whether any file referenced by m is open in +// the snapshot s. +// +// s.mu must be held while calling this function. +func containsOpenFileLocked(s *snapshot, m *KnownMetadata) bool { + uris := map[span.URI]struct{}{} + for _, uri := range m.CompiledGoFiles { + uris[uri] = struct{}{} + } + for _, uri := range m.GoFiles { + uris[uri] = struct{}{} + } + + for uri := range uris { + if s.isOpenLocked(uri) { + return true + } + } + return false +} + +// containsFileInWorkspaceLocked reports whether m contains any file inside the +// workspace of the snapshot s. +// +// s.mu must be held while calling this function. +func containsFileInWorkspaceLocked(s *snapshot, m *Metadata) bool { + uris := map[span.URI]struct{}{} + for _, uri := range m.CompiledGoFiles { + uris[uri] = struct{}{} + } + for _, uri := range m.GoFiles { + uris[uri] = struct{}{} + } + + for uri := range uris { + // In order for a package to be considered for the workspace, at least one + // file must be contained in the workspace and not vendored. + + // The package's files are in this view. It may be a workspace package. + // Vendored packages are not likely to be interesting to the user. + if !strings.Contains(string(uri), "/vendor/") && s.view.contains(uri) { + return true + } + } + return false +} + +// computeWorkspacePackagesLocked computes workspace packages in the snapshot s +// for the given metadata graph. +// +// s.mu must be held while calling this function. +func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[PackageID]PackagePath { + workspacePackages := make(map[PackageID]PackagePath) + for _, m := range meta.metadata { + // Don't consider invalid packages to be workspace packages. Doing so can + // result in type-checking and diagnosing packages that no longer exist, + // which can lead to memory leaks and confusing errors. + if !m.Valid { + continue + } + + if !containsPackageLocked(s, m.Metadata) { + continue + } + + if source.IsCommandLineArguments(string(m.ID)) { + // If all the files contained in m have a real package, we don't need to + // keep m as a workspace package. + if allFilesHaveRealPackages(meta, m) { + continue + } + + // We only care about command-line-arguments packages if they are still + // open. + if !containsOpenFileLocked(s, m) { + continue + } + } + + switch { + case m.ForTest == "": + // A normal package. + workspacePackages[m.ID] = m.PkgPath + case m.ForTest == m.PkgPath, m.ForTest+"_test" == m.PkgPath: + // The test variant of some workspace package or its x_test. + // To load it, we need to load the non-test variant with -test. + // + // Notably, this excludes intermediate test variants from workspace + // packages. + workspacePackages[m.ID] = m.ForTest + } + } + return workspacePackages +} + +// allFilesHaveRealPackages reports whether all files referenced by m are +// contained in a "real" package (not command-line-arguments). +// +// If m is valid but all "real" packages containing any file are invalid, this +// function returns false. +// +// If m is not a command-line-arguments package, this is trivially true. +func allFilesHaveRealPackages(g *metadataGraph, m *KnownMetadata) bool { + n := len(m.CompiledGoFiles) +checkURIs: + for _, uri := range append(m.CompiledGoFiles[0:n:n], m.GoFiles...) { + for _, id := range g.ids[uri] { + if !source.IsCommandLineArguments(string(id)) && (g.metadata[id].Valid || !m.Valid) { + continue checkURIs + } + } + return false + } + return true +} + +func isTestMain(pkg *packages.Package, gocache string) bool { + // Test mains must have an import path that ends with ".test". + if !strings.HasSuffix(pkg.PkgPath, ".test") { + return false + } + // Test main packages are always named "main". + if pkg.Name != "main" { + return false + } + // Test mains always have exactly one GoFile that is in the build cache. + if len(pkg.GoFiles) > 1 { + return false + } + if !source.InDir(gocache, pkg.GoFiles[0]) { + return false + } + return true +} diff --git a/gopls/internal/lsp/cache/maps.go b/gopls/internal/lsp/cache/maps.go new file mode 100644 index 00000000000..edb8d168f24 --- /dev/null +++ b/gopls/internal/lsp/cache/maps.go @@ -0,0 +1,216 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/persistent" +) + +// TODO(euroelessar): Use generics once support for go1.17 is dropped. + +type filesMap struct { + impl *persistent.Map +} + +// uriLessInterface is the < relation for "any" values containing span.URIs. +func uriLessInterface(a, b interface{}) bool { + return a.(span.URI) < b.(span.URI) +} + +func newFilesMap() filesMap { + return filesMap{ + impl: persistent.NewMap(uriLessInterface), + } +} + +func (m filesMap) Clone() filesMap { + return filesMap{ + impl: m.impl.Clone(), + } +} + +func (m filesMap) Destroy() { + m.impl.Destroy() +} + +func (m filesMap) Get(key span.URI) (source.VersionedFileHandle, bool) { + value, ok := m.impl.Get(key) + if !ok { + return nil, false + } + return value.(source.VersionedFileHandle), true +} + +func (m filesMap) Range(do func(key span.URI, value source.VersionedFileHandle)) { + m.impl.Range(func(key, value interface{}) { + do(key.(span.URI), value.(source.VersionedFileHandle)) + }) +} + +func (m filesMap) Set(key span.URI, value source.VersionedFileHandle) { + m.impl.Set(key, value, nil) +} + +func (m filesMap) Delete(key span.URI) { + m.impl.Delete(key) +} + +func parseKeyLessInterface(a, b interface{}) bool { + return parseKeyLess(a.(parseKey), b.(parseKey)) +} + +func parseKeyLess(a, b parseKey) bool { + if a.mode != b.mode { + return a.mode < b.mode + } + if a.file.Hash != b.file.Hash { + return a.file.Hash.Less(b.file.Hash) + } + return a.file.URI < b.file.URI +} + +type isActivePackageCacheMap struct { + impl *persistent.Map +} + +func newIsActivePackageCacheMap() isActivePackageCacheMap { + return isActivePackageCacheMap{ + impl: persistent.NewMap(func(a, b interface{}) bool { + return a.(PackageID) < b.(PackageID) + }), + } +} + +func (m isActivePackageCacheMap) Clone() isActivePackageCacheMap { + return isActivePackageCacheMap{ + impl: m.impl.Clone(), + } +} + +func (m isActivePackageCacheMap) Destroy() { + m.impl.Destroy() +} + +func (m isActivePackageCacheMap) Get(key PackageID) (bool, bool) { + value, ok := m.impl.Get(key) + if !ok { + return false, false + } + return value.(bool), true +} + +func (m isActivePackageCacheMap) Set(key PackageID, value bool) { + m.impl.Set(key, value, nil) +} + +type parseKeysByURIMap struct { + impl *persistent.Map +} + +func newParseKeysByURIMap() parseKeysByURIMap { + return parseKeysByURIMap{ + impl: persistent.NewMap(uriLessInterface), + } +} + +func (m parseKeysByURIMap) Clone() parseKeysByURIMap { + return parseKeysByURIMap{ + impl: m.impl.Clone(), + } +} + +func (m parseKeysByURIMap) Destroy() { + m.impl.Destroy() +} + +func (m parseKeysByURIMap) Get(key span.URI) ([]parseKey, bool) { + value, ok := m.impl.Get(key) + if !ok { + return nil, false + } + return value.([]parseKey), true +} + +func (m parseKeysByURIMap) Range(do func(key span.URI, value []parseKey)) { + m.impl.Range(func(key, value interface{}) { + do(key.(span.URI), value.([]parseKey)) + }) +} + +func (m parseKeysByURIMap) Set(key span.URI, value []parseKey) { + m.impl.Set(key, value, nil) +} + +func (m parseKeysByURIMap) Delete(key span.URI) { + m.impl.Delete(key) +} + +func packageKeyLessInterface(x, y interface{}) bool { + return packageKeyLess(x.(packageKey), y.(packageKey)) +} + +func packageKeyLess(x, y packageKey) bool { + if x.mode != y.mode { + return x.mode < y.mode + } + return x.id < y.id +} + +type knownDirsSet struct { + impl *persistent.Map +} + +func newKnownDirsSet() knownDirsSet { + return knownDirsSet{ + impl: persistent.NewMap(func(a, b interface{}) bool { + return a.(span.URI) < b.(span.URI) + }), + } +} + +func (s knownDirsSet) Clone() knownDirsSet { + return knownDirsSet{ + impl: s.impl.Clone(), + } +} + +func (s knownDirsSet) Destroy() { + s.impl.Destroy() +} + +func (s knownDirsSet) Contains(key span.URI) bool { + _, ok := s.impl.Get(key) + return ok +} + +func (s knownDirsSet) Range(do func(key span.URI)) { + s.impl.Range(func(key, value interface{}) { + do(key.(span.URI)) + }) +} + +func (s knownDirsSet) SetAll(other knownDirsSet) { + s.impl.SetAll(other.impl) +} + +func (s knownDirsSet) Insert(key span.URI) { + s.impl.Set(key, nil, nil) +} + +func (s knownDirsSet) Remove(key span.URI) { + s.impl.Delete(key) +} + +// actionKeyLessInterface is the less-than relation for actionKey +// values wrapped in an interface. +func actionKeyLessInterface(a, b interface{}) bool { + x, y := a.(actionKey), b.(actionKey) + if x.analyzer.Name != y.analyzer.Name { + return x.analyzer.Name < y.analyzer.Name + } + return x.pkgid < y.pkgid +} diff --git a/gopls/internal/lsp/cache/metadata.go b/gopls/internal/lsp/cache/metadata.go new file mode 100644 index 00000000000..c8b0a537222 --- /dev/null +++ b/gopls/internal/lsp/cache/metadata.go @@ -0,0 +1,106 @@ +// 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 cache + +import ( + "go/types" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/packagesinternal" +) + +// Declare explicit types for package paths, names, and IDs to ensure that we +// never use an ID where a path belongs, and vice versa. If we confused these, +// it would result in confusing errors because package IDs often look like +// package paths. +type ( + PackageID string // go list's unique identifier for a package (e.g. "vendor/example.com/foo [vendor/example.com/bar.test]") + PackagePath string // name used to prefix linker symbols (e.g. "vendor/example.com/foo") + PackageName string // identifier in 'package' declaration (e.g. "foo") + ImportPath string // path that appears in an import declaration (e.g. "example.com/foo") +) + +// Metadata holds package Metadata extracted from a call to packages.Load. +type Metadata struct { + ID PackageID + PkgPath PackagePath + Name PackageName + GoFiles []span.URI + CompiledGoFiles []span.URI + ForTest PackagePath // package path under test, or "" + TypesSizes types.Sizes + Errors []packages.Error + DepsByImpPath map[ImportPath]PackageID // may contain dups; empty ID => missing + DepsByPkgPath map[PackagePath]PackageID // values are unique and non-empty + Module *packages.Module + depsErrors []*packagesinternal.PackageError + + // Config is the *packages.Config associated with the loaded package. + Config *packages.Config +} + +// PackageID implements the source.Metadata interface. +func (m *Metadata) PackageID() string { + return string(m.ID) +} + +// Name implements the source.Metadata interface. +func (m *Metadata) PackageName() string { + return string(m.Name) +} + +// PkgPath implements the source.Metadata interface. +func (m *Metadata) PackagePath() string { + return string(m.PkgPath) +} + +// IsIntermediateTestVariant reports whether the given package is an +// intermediate test variant, e.g. "net/http [net/url.test]". +// +// Such test variants arise when an x_test package (in this case net/url_test) +// imports a package (in this case net/http) that itself imports the the +// non-x_test package (in this case net/url). +// +// This is done so that the forward transitive closure of net/url_test has +// only one package for the "net/url" import. +// The intermediate test variant exists to hold the test variant import: +// +// net/url_test [net/url.test] +// +// | "net/http" -> net/http [net/url.test] +// | "net/url" -> net/url [net/url.test] +// | ... +// +// net/http [net/url.test] +// +// | "net/url" -> net/url [net/url.test] +// | ... +// +// This restriction propagates throughout the import graph of net/http: for +// every package imported by net/http that imports net/url, there must be an +// intermediate test variant that instead imports "net/url [net/url.test]". +// +// As one can see from the example of net/url and net/http, intermediate test +// variants can result in many additional packages that are essentially (but +// not quite) identical. For this reason, we filter these variants wherever +// possible. +func (m *Metadata) IsIntermediateTestVariant() bool { + return m.ForTest != "" && m.ForTest != m.PkgPath && m.ForTest+"_test" != m.PkgPath +} + +// ModuleInfo implements the source.Metadata interface. +func (m *Metadata) ModuleInfo() *packages.Module { + return m.Module +} + +// KnownMetadata is a wrapper around metadata that tracks its validity. +type KnownMetadata struct { + *Metadata + + // Valid is true if the given metadata is Valid. + // Invalid metadata can still be used if a metadata reload fails. + Valid bool +} diff --git a/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go similarity index 60% rename from internal/lsp/cache/mod.go rename to gopls/internal/lsp/cache/mod.go index 5ac199bd96b..97bdb2f019a 100644 --- a/internal/lsp/cache/mod.go +++ b/gopls/internal/lsp/cache/mod.go @@ -14,162 +14,166 @@ import ( "golang.org/x/mod/modfile" "golang.org/x/mod/module" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) -type parseModHandle struct { - handle *memoize.Handle -} +// ParseMod parses a go.mod file, using a cache. It may return partial results and an error. +func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) { + uri := fh.URI() -type parseModData struct { - parsed *source.ParsedModule + s.mu.Lock() + entry, hit := s.parseModHandles.Get(uri) + s.mu.Unlock() - // err is any error encountered while parsing the file. - err error -} + type parseModResult struct { + parsed *source.ParsedModule + err error + } -func (mh *parseModHandle) parse(ctx context.Context, snapshot *snapshot) (*source.ParsedModule, error) { - v, err := mh.handle.Get(ctx, snapshot.generation, snapshot) + // cache miss? + if !hit { + promise, release := s.store.Promise(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { + parsed, err := parseModImpl(ctx, fh) + return parseModResult{parsed, err} + }) + + entry = promise + s.mu.Lock() + s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.mu.Unlock() + } + + // Await result. + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } - data := v.(*parseModData) - return data.parsed, data.err + res := v.(parseModResult) + return res.parsed, res.err } -func (s *snapshot) ParseMod(ctx context.Context, modFH source.FileHandle) (*source.ParsedModule, error) { - if handle := s.getParseModHandle(modFH.URI()); handle != nil { - return handle.parse(ctx, s) - } - h := s.generation.Bind(modFH.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} { - _, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI())) - defer done() +// parseModImpl parses the go.mod file whose name and contents are in fh. +// It may return partial results and an error. +func parseModImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) { + _, done := event.Start(ctx, "cache.ParseMod", tag.URI.Of(fh.URI())) + defer done() - contents, err := modFH.Read() - if err != nil { - return &parseModData{err: err} - } - m := protocol.NewColumnMapper(modFH.URI(), contents) - file, parseErr := modfile.Parse(modFH.URI().Filename(), contents, nil) - // Attempt to convert the error to a standardized parse error. - var parseErrors []*source.Diagnostic - if parseErr != nil { - mfErrList, ok := parseErr.(modfile.ErrorList) - if !ok { - return &parseModData{err: fmt.Errorf("unexpected parse error type %v", parseErr)} - } - for _, mfErr := range mfErrList { - rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos) - if err != nil { - return &parseModData{err: err} - } - parseErrors = append(parseErrors, &source.Diagnostic{ - URI: modFH.URI(), - Range: rng, - Severity: protocol.SeverityError, - Source: source.ParseError, - Message: mfErr.Err.Error(), - }) + contents, err := fh.Read() + if err != nil { + return nil, err + } + m := protocol.NewColumnMapper(fh.URI(), contents) + file, parseErr := modfile.Parse(fh.URI().Filename(), contents, nil) + // Attempt to convert the error to a standardized parse error. + var parseErrors []*source.Diagnostic + if parseErr != nil { + mfErrList, ok := parseErr.(modfile.ErrorList) + if !ok { + return nil, fmt.Errorf("unexpected parse error type %v", parseErr) + } + for _, mfErr := range mfErrList { + rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) + if err != nil { + return nil, err } + parseErrors = append(parseErrors, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityError, + Source: source.ParseError, + Message: mfErr.Err.Error(), + }) } - return &parseModData{ - parsed: &source.ParsedModule{ - URI: modFH.URI(), - Mapper: m, - File: file, - ParseErrors: parseErrors, - }, - err: parseErr, - } - }, nil) + } + return &source.ParsedModule{ + URI: fh.URI(), + Mapper: m, + File: file, + ParseErrors: parseErrors, + }, parseErr +} + +// ParseWork parses a go.work file, using a cache. It may return partial results and an error. +// TODO(adonovan): move to new work.go file. +func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) { + uri := fh.URI() - pmh := &parseModHandle{handle: h} s.mu.Lock() - s.parseModHandles[modFH.URI()] = pmh + entry, hit := s.parseWorkHandles.Get(uri) s.mu.Unlock() - return pmh.parse(ctx, s) -} - -type parseWorkHandle struct { - handle *memoize.Handle -} + type parseWorkResult struct { + parsed *source.ParsedWorkFile + err error + } -type parseWorkData struct { - parsed *source.ParsedWorkFile + // cache miss? + if !hit { + handle, release := s.store.Promise(fh.FileIdentity(), func(ctx context.Context, _ interface{}) interface{} { + parsed, err := parseWorkImpl(ctx, fh) + return parseWorkResult{parsed, err} + }) - // err is any error encountered while parsing the file. - err error -} + entry = handle + s.mu.Lock() + s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.mu.Unlock() + } -func (mh *parseWorkHandle) parse(ctx context.Context, snapshot *snapshot) (*source.ParsedWorkFile, error) { - v, err := mh.handle.Get(ctx, snapshot.generation, snapshot) + // Await result. + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } - data := v.(*parseWorkData) - return data.parsed, data.err + res := v.(parseWorkResult) + return res.parsed, res.err } -func (s *snapshot) ParseWork(ctx context.Context, modFH source.FileHandle) (*source.ParsedWorkFile, error) { - if handle := s.getParseWorkHandle(modFH.URI()); handle != nil { - return handle.parse(ctx, s) - } - h := s.generation.Bind(modFH.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} { - _, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI())) - defer done() +// parseWorkImpl parses a go.work file. It may return partial results and an error. +func parseWorkImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) { + _, done := event.Start(ctx, "cache.ParseWork", tag.URI.Of(fh.URI())) + defer done() - contents, err := modFH.Read() - if err != nil { - return &parseWorkData{err: err} - } - m := protocol.NewColumnMapper(modFH.URI(), contents) - file, parseErr := modfile.ParseWork(modFH.URI().Filename(), contents, nil) - // Attempt to convert the error to a standardized parse error. - var parseErrors []*source.Diagnostic - if parseErr != nil { - mfErrList, ok := parseErr.(modfile.ErrorList) - if !ok { - return &parseWorkData{err: fmt.Errorf("unexpected parse error type %v", parseErr)} - } - for _, mfErr := range mfErrList { - rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos) - if err != nil { - return &parseWorkData{err: err} - } - parseErrors = append(parseErrors, &source.Diagnostic{ - URI: modFH.URI(), - Range: rng, - Severity: protocol.SeverityError, - Source: source.ParseError, - Message: mfErr.Err.Error(), - }) + contents, err := fh.Read() + if err != nil { + return nil, err + } + m := protocol.NewColumnMapper(fh.URI(), contents) + file, parseErr := modfile.ParseWork(fh.URI().Filename(), contents, nil) + // Attempt to convert the error to a standardized parse error. + var parseErrors []*source.Diagnostic + if parseErr != nil { + mfErrList, ok := parseErr.(modfile.ErrorList) + if !ok { + return nil, fmt.Errorf("unexpected parse error type %v", parseErr) + } + for _, mfErr := range mfErrList { + rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) + if err != nil { + return nil, err } + parseErrors = append(parseErrors, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityError, + Source: source.ParseError, + Message: mfErr.Err.Error(), + }) } - return &parseWorkData{ - parsed: &source.ParsedWorkFile{ - URI: modFH.URI(), - Mapper: m, - File: file, - ParseErrors: parseErrors, - }, - err: parseErr, - } - }, nil) - - pwh := &parseWorkHandle{handle: h} - s.mu.Lock() - s.parseWorkHandles[modFH.URI()] = pwh - s.mu.Unlock() - - return pwh.parse(ctx, s) + } + return &source.ParsedWorkFile{ + URI: fh.URI(), + Mapper: m, + File: file, + ParseErrors: parseErrors, + }, parseErr } // goSum reads the go.sum file for the go.mod file at modURI, if it exists. If @@ -198,102 +202,82 @@ func sumFilename(modURI span.URI) string { return strings.TrimSuffix(modURI.Filename(), ".mod") + ".sum" } -// modKey is uniquely identifies cached data for `go mod why` or dependencies -// to upgrade. -type modKey struct { - sessionID, env, view string - mod source.FileIdentity - verb modAction -} +// ModWhy returns the "go mod why" result for each module named in a +// require statement in the go.mod file. +// TODO(adonovan): move to new mod_why.go file. +func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) { + uri := fh.URI() -type modAction int + if s.View().FileKind(fh) != source.Mod { + return nil, fmt.Errorf("%s is not a go.mod file", uri) + } -const ( - why modAction = iota - upgrade -) + s.mu.Lock() + entry, hit := s.modWhyHandles.Get(uri) + s.mu.Unlock() -type modWhyHandle struct { - handle *memoize.Handle -} + type modWhyResult struct { + why map[string]string + err error + } -type modWhyData struct { - // why keeps track of the `go mod why` results for each require statement - // in the go.mod file. - why map[string]string + // cache miss? + if !hit { + handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg interface{}) interface{} { + why, err := modWhyImpl(ctx, arg.(*snapshot), fh) + return modWhyResult{why, err} + }) - err error -} + entry = handle + s.mu.Lock() + s.modWhyHandles.Set(uri, entry, nil) + s.mu.Unlock() + } -func (mwh *modWhyHandle) why(ctx context.Context, snapshot *snapshot) (map[string]string, error) { - v, err := mwh.handle.Get(ctx, snapshot.generation, snapshot) + // Await result. + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } - data := v.(*modWhyData) - return data.why, data.err + res := v.(modWhyResult) + return res.why, res.err } -func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) { - if s.View().FileKind(fh) != source.Mod { - return nil, fmt.Errorf("%s is not a go.mod file", fh.URI()) +// modWhyImpl returns the result of "go mod why -m" on the specified go.mod file. +func modWhyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle) (map[string]string, error) { + ctx, done := event.Start(ctx, "cache.ModWhy", tag.URI.Of(fh.URI())) + defer done() + + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil { + return nil, err } - if handle := s.getModWhyHandle(fh.URI()); handle != nil { - return handle.why(ctx, s) + // No requires to explain. + if len(pm.File.Require) == 0 { + return nil, nil // empty result } - key := modKey{ - sessionID: s.view.session.id, - env: hashEnv(s), - mod: fh.FileIdentity(), - view: s.view.rootURI.Filename(), - verb: why, + // Run `go mod why` on all the dependencies. + inv := &gocommand.Invocation{ + Verb: "mod", + Args: []string{"why", "-m"}, + WorkingDir: filepath.Dir(fh.URI().Filename()), } - h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { - ctx, done := event.Start(ctx, "cache.ModWhyHandle", tag.URI.Of(fh.URI())) - defer done() - - snapshot := arg.(*snapshot) - - pm, err := snapshot.ParseMod(ctx, fh) - if err != nil { - return &modWhyData{err: err} - } - // No requires to explain. - if len(pm.File.Require) == 0 { - return &modWhyData{} - } - // Run `go mod why` on all the dependencies. - inv := &gocommand.Invocation{ - Verb: "mod", - Args: []string{"why", "-m"}, - WorkingDir: filepath.Dir(fh.URI().Filename()), - } - for _, req := range pm.File.Require { - inv.Args = append(inv.Args, req.Mod.Path) - } - stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal, inv) - if err != nil { - return &modWhyData{err: err} - } - whyList := strings.Split(stdout.String(), "\n\n") - if len(whyList) != len(pm.File.Require) { - return &modWhyData{ - err: fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)), - } - } - why := make(map[string]string, len(pm.File.Require)) - for i, req := range pm.File.Require { - why[req.Mod.Path] = whyList[i] - } - return &modWhyData{why: why} - }, nil) - - mwh := &modWhyHandle{handle: h} - s.mu.Lock() - s.modWhyHandles[fh.URI()] = mwh - s.mu.Unlock() - - return mwh.why(ctx, s) + for _, req := range pm.File.Require { + inv.Args = append(inv.Args, req.Mod.Path) + } + stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal, inv) + if err != nil { + return nil, err + } + whyList := strings.Split(stdout.String(), "\n\n") + if len(whyList) != len(pm.File.Require) { + return nil, fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)) + } + why := make(map[string]string, len(pm.File.Require)) + for i, req := range pm.File.Require { + why[req.Mod.Path] = whyList[i] + } + return why, nil } // extractGoCommandError tries to parse errors that come from the go command diff --git a/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go similarity index 66% rename from internal/lsp/cache/mod_tidy.go rename to gopls/internal/lsp/cache/mod_tidy.go index aa525e7413d..cc604c1c86f 100644 --- a/internal/lsp/cache/mod_tidy.go +++ b/gopls/internal/lsp/cache/mod_tidy.go @@ -8,170 +8,138 @@ import ( "context" "fmt" "go/ast" + "go/token" "io/ioutil" "os" "path/filepath" - "sort" "strconv" "strings" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) -type modTidyKey struct { - sessionID string - env string - gomod source.FileIdentity - imports string - unsavedOverlays string - view string -} +// ModTidy returns the go.mod file that would be obtained by running +// "go mod tidy". Concurrent requests are combined into a single command. +func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { + uri := pm.URI + if pm.File == nil { + return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", uri) + } -type modTidyHandle struct { - handle *memoize.Handle -} + s.mu.Lock() + entry, hit := s.modTidyHandles.Get(uri) + s.mu.Unlock() -type modTidyData struct { - tidied *source.TidiedModule - err error -} + type modTidyResult struct { + tidied *source.TidiedModule + err error + } + + // Cache miss? + if !hit { + // If the file handle is an overlay, it may not be written to disk. + // The go.mod file has to be on disk for `go mod tidy` to work. + // TODO(rfindley): is this still true with Go 1.16 overlay support? + fh, err := s.GetFile(ctx, pm.URI) + if err != nil { + return nil, err + } + if _, ok := fh.(*overlay); ok { + if info, _ := os.Stat(uri.Filename()); info == nil { + return nil, source.ErrNoModOnDisk + } + } + + if criticalErr := s.GetCriticalError(ctx); criticalErr != nil { + return &source.TidiedModule{ + Diagnostics: criticalErr.Diagnostics, + }, nil + } -func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) { - v, err := mth.handle.Get(ctx, snapshot.generation, snapshot) + if err := s.awaitLoaded(ctx); err != nil { + return nil, err + } + + handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} { + tidied, err := modTidyImpl(ctx, arg.(*snapshot), uri.Filename(), pm) + return modTidyResult{tidied, err} + }) + + entry = handle + s.mu.Lock() + s.modTidyHandles.Set(uri, entry, nil) + s.mu.Unlock() + } + + // Await result. + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } - data := v.(*modTidyData) - return data.tidied, data.err + res := v.(modTidyResult) + return res.tidied, res.err } -func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { - if pm.File == nil { - return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI) - } - if handle := s.getModTidyHandle(pm.URI); handle != nil { - return handle.tidy(ctx, s) +// modTidyImpl runs "go mod tidy" on a go.mod file. +func modTidyImpl(ctx context.Context, snapshot *snapshot, filename string, pm *source.ParsedModule) (*source.TidiedModule, error) { + ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(filename)) + defer done() + + inv := &gocommand.Invocation{ + Verb: "mod", + Args: []string{"tidy"}, + WorkingDir: filepath.Dir(filename), } - fh, err := s.GetFile(ctx, pm.URI) + // TODO(adonovan): ensure that unsaved overlays are passed through to 'go'. + tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv) if err != nil { return nil, err } - // If the file handle is an overlay, it may not be written to disk. - // The go.mod file has to be on disk for `go mod tidy` to work. - if _, ok := fh.(*overlay); ok { - if info, _ := os.Stat(fh.URI().Filename()); info == nil { - return nil, source.ErrNoModOnDisk - } - } - if criticalErr := s.GetCriticalError(ctx); criticalErr != nil { - return &source.TidiedModule{ - Diagnostics: criticalErr.DiagList, - }, nil + // Keep the temporary go.mod file around long enough to parse it. + defer cleanup() + + if _, err := snapshot.view.session.gocmdRunner.Run(ctx, *inv); err != nil { + return nil, err } - workspacePkgs, err := s.workspacePackageHandles(ctx) + + // Go directly to disk to get the temporary mod file, + // since it is always on disk. + tempContents, err := ioutil.ReadFile(tmpURI.Filename()) if err != nil { return nil, err } - importHash, err := s.hashImports(ctx, workspacePkgs) + ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil) if err != nil { + // We do not need to worry about the temporary file's parse errors + // since it has been "tidied". return nil, err } - s.mu.Lock() - overlayHash := hashUnsavedOverlays(s.files) - s.mu.Unlock() - - key := modTidyKey{ - sessionID: s.view.session.id, - view: s.view.folder.Filename(), - imports: importHash, - unsavedOverlays: overlayHash, - gomod: fh.FileIdentity(), - env: hashEnv(s), - } - h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { - ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI())) - defer done() - - snapshot := arg.(*snapshot) - inv := &gocommand.Invocation{ - Verb: "mod", - Args: []string{"tidy"}, - WorkingDir: filepath.Dir(fh.URI().Filename()), - } - tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv) - if err != nil { - return &modTidyData{err: err} - } - // Keep the temporary go.mod file around long enough to parse it. - defer cleanup() - - if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil { - return &modTidyData{err: err} - } - // Go directly to disk to get the temporary mod file, since it is - // always on disk. - tempContents, err := ioutil.ReadFile(tmpURI.Filename()) - if err != nil { - return &modTidyData{err: err} - } - ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil) - if err != nil { - // We do not need to worry about the temporary file's parse errors - // since it has been "tidied". - return &modTidyData{err: err} - } - // Compare the original and tidied go.mod files to compute errors and - // suggested fixes. - diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs) - if err != nil { - return &modTidyData{err: err} - } - return &modTidyData{ - tidied: &source.TidiedModule{ - Diagnostics: diagnostics, - TidiedContent: tempContents, - }, - } - }, nil) - - mth := &modTidyHandle{handle: h} - s.mu.Lock() - s.modTidyHandles[fh.URI()] = mth - s.mu.Unlock() - - return mth.tidy(ctx, s) -} - -func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) (string, error) { - seen := map[string]struct{}{} - var imports []string - for _, ph := range wsPackages { - for _, imp := range ph.imports(ctx, s) { - if _, ok := seen[imp]; !ok { - imports = append(imports, imp) - seen[imp] = struct{}{} - } - } + // Compare the original and tidied go.mod files to compute errors and + // suggested fixes. + diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal) + if err != nil { + return nil, err } - sort.Strings(imports) - hashed := strings.Join(imports, ",") - return hashContents([]byte(hashed)), nil + + return &source.TidiedModule{ + Diagnostics: diagnostics, + TidiedContent: tempContents, + }, nil } // modTidyDiagnostics computes the differences between the original and tidied // go.mod files to produce diagnostic and suggested fixes. Some diagnostics // may appear on the Go files that import packages from missing modules. -func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []*packageHandle) (diagnostics []*source.Diagnostic, err error) { +func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File) (diagnostics []*source.Diagnostic, err error) { // First, determine which modules are unused and which are missing from the // original go.mod file. var ( @@ -220,15 +188,25 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc } // Add diagnostics for missing modules anywhere they are imported in the // workspace. - for _, ph := range workspacePkgs { + // TODO(adonovan): opt: opportunities for parallelism abound. + for _, id := range snapshot.workspacePackageIDs() { + m := snapshot.getMetadata(id) + if m == nil { + return nil, fmt.Errorf("no metadata for %s", id) + } + + // Read both lists of files of this package, in parallel. + goFiles, compiledGoFiles, err := readGoFiles(ctx, snapshot, m.Metadata) + if err != nil { + return nil, err + } + missingImports := map[string]*modfile.Require{} // If -mod=readonly is not set we may have successfully imported // packages from missing modules. Otherwise they'll be in // MissingDependencies. Combine both. - importedPkgs := ph.imports(ctx, snapshot) - - for _, imp := range importedPkgs { + for imp := range parseImports(ctx, snapshot, goFiles) { if req, ok := missing[imp]; ok { missingImports[imp] = req break @@ -257,8 +235,8 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc if len(missingImports) == 0 { continue } - for _, pgh := range ph.compiledGoFiles { - pgf, err := snapshot.ParseGo(ctx, pgh.file, source.ParseHeader) + for _, goFile := range compiledGoFiles { + pgf, err := snapshot.ParseGo(ctx, goFile, source.ParseHeader) if err != nil { continue } @@ -287,7 +265,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc if !ok { return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path) } - srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes) + srcErr, err := missingModuleForImport(pgf.Tok, m, imp, req, fixes) if err != nil { return nil, err } @@ -309,7 +287,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc // unusedDiagnostic returns a source.Diagnostic for an unused require. func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) { - rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) + rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err } @@ -334,8 +312,8 @@ func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagno // directnessDiagnostic extracts errors when a dependency is labeled indirect when // it should be direct and vice versa. -func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Diagnostic, error) { - rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End) +func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) { + rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) if err != nil { return nil, err } @@ -347,8 +325,8 @@ func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, comput if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 { end := comments.Suffix[0].Start end.LineRune += len(comments.Suffix[0].Token) - end.Byte += len([]byte(comments.Suffix[0].Token)) - rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end) + end.Byte += len(comments.Suffix[0].Token) + rng, err = m.OffsetRange(comments.Suffix[0].Start.Byte, end.Byte) if err != nil { return nil, err } @@ -381,7 +359,7 @@ func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*so if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil { start, end := pm.File.Module.Syntax.Span() var err error - rng, err = rangeFromPositions(pm.Mapper, start, end) + rng, err = pm.Mapper.OffsetRange(start.Byte, end.Byte) if err != nil { return nil, err } @@ -407,7 +385,7 @@ func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*so // switchDirectness gets the edits needed to change an indirect dependency to // direct and vice versa. -func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) { +func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits source.DiffFunction) ([]protocol.TextEdit, error) { // We need a private copy of the parsed go.mod file, since we're going to // modify it. copied, err := modfile.Parse("", m.Content, nil) @@ -441,24 +419,17 @@ func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdi return nil, err } // Calculate the edits to be made due to the change. - diff, err := computeEdits(m.URI, string(m.Content), string(newContent)) - if err != nil { - return nil, err - } - return source.ToProtocolEdits(m, diff) + edits := computeEdits(string(m.Content), string(newContent)) + return source.ToProtocolEdits(m, edits) } // missingModuleForImport creates an error for a given import path that comes // from a missing module. -func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) { +func missingModuleForImport(file *token.File, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) { if req.Syntax == nil { return nil, fmt.Errorf("no syntax for %v", req) } - spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span() - if err != nil { - return nil, err - } - rng, err := m.Range(spn) + rng, err := m.PosRange(imp.Path.Pos(), imp.Path.End()) if err != nil { return nil, err } @@ -472,14 +443,6 @@ func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, }, nil } -func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) { - spn, err := spanFromPositions(m, s, e) - if err != nil { - return protocol.Range{}, err - } - return m.Range(spn) -} - func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) { toPoint := func(offset int) (span.Point, error) { l, c, err := span.ToPosition(m.TokFile, offset) @@ -498,3 +461,26 @@ func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Sp } return span.New(m.URI, start, end), nil } + +// parseImports parses the headers of the specified files and returns +// the set of strings that appear in import declarations within +// GoFiles. Errors are ignored. +// +// (We can't simply use Metadata.Imports because it is based on +// CompiledGoFiles, after cgo processing.) +func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) map[string]bool { + s.mu.Lock() // peekOrParse requires a locked snapshot (!) + defer s.mu.Unlock() + seen := make(map[string]bool) + for _, file := range files { + f, err := peekOrParse(ctx, s, file, source.ParseHeader) + if err != nil { + continue + } + for _, spec := range f.File.Imports { + path, _ := strconv.Unquote(spec.Path.Value) + seen[path] = true + } + } + return seen +} diff --git a/internal/lsp/cache/os_darwin.go b/gopls/internal/lsp/cache/os_darwin.go similarity index 100% rename from internal/lsp/cache/os_darwin.go rename to gopls/internal/lsp/cache/os_darwin.go diff --git a/internal/lsp/cache/os_windows.go b/gopls/internal/lsp/cache/os_windows.go similarity index 100% rename from internal/lsp/cache/os_windows.go rename to gopls/internal/lsp/cache/os_windows.go diff --git a/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go similarity index 78% rename from internal/lsp/cache/parse.go rename to gopls/internal/lsp/cache/parse.go index 668c437f5c9..5bac8713153 100644 --- a/internal/lsp/cache/parse.go +++ b/gopls/internal/lsp/cache/parse.go @@ -18,15 +18,13 @@ import ( "strconv" "strings" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" ) // parseKey uniquely identifies a parsed Go file. @@ -35,233 +33,90 @@ type parseKey struct { mode source.ParseMode } -type parseGoHandle struct { - handle *memoize.Handle - file source.FileHandle - mode source.ParseMode -} - -type parseGoData struct { - parsed *source.ParsedGoFile - - // If true, we adjusted the AST to make it type check better, and - // it may not match the source code. - fixed bool - err error // any other errors -} - -func (s *snapshot) parseGoHandle(ctx context.Context, fh source.FileHandle, mode source.ParseMode) *parseGoHandle { - key := parseKey{ - file: fh.FileIdentity(), - mode: mode, - } - if pgh := s.getGoFile(key); pgh != nil { - return pgh - } - parseHandle := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { - snapshot := arg.(*snapshot) - return parseGo(ctx, snapshot.FileSet(), fh, mode) - }, nil) - - pgh := &parseGoHandle{ - handle: parseHandle, - file: fh, - mode: mode, - } - return s.addGoFile(key, pgh) -} - -func (pgh *parseGoHandle) String() string { - return pgh.file.URI().Filename() -} - +// ParseGo parses the file whose contents are provided by fh, using a cache. +// The resulting tree may have be fixed up. +// +// The parser mode must not be ParseExported: that mode is used during +// type checking to destructively trim the tree to reduce work, +// which is not safe for values from a shared cache. +// TODO(adonovan): opt: shouldn't parseGoImpl do the trimming? +// Then we can cache the result since it would never change. func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { - pgf, _, err := s.parseGo(ctx, fh, mode) - return pgf, err -} - -func (s *snapshot) parseGo(ctx context.Context, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, bool, error) { if mode == source.ParseExported { panic("only type checking should use Exported") } - pgh := s.parseGoHandle(ctx, fh, mode) - d, err := pgh.handle.Get(ctx, s.generation, s) - if err != nil { - return nil, false, err - } - data := d.(*parseGoData) - return data.parsed, data.fixed, data.err -} -// cachedPGF returns the cached ParsedGoFile for the given ParseMode, if it -// has already been computed. Otherwise, it returns nil. -func (s *snapshot) cachedPGF(fh source.FileHandle, mode source.ParseMode) *source.ParsedGoFile { - key := parseKey{file: fh.FileIdentity(), mode: mode} - if pgh := s.getGoFile(key); pgh != nil { - cached := pgh.handle.Cached(s.generation) - if cached != nil { - cached := cached.(*parseGoData) - if cached.parsed != nil { - return cached.parsed - } - } + key := parseKey{ + file: fh.FileIdentity(), + mode: mode, } - return nil -} - -type astCacheKey struct { - pkg packageHandleKey - uri span.URI -} -func (s *snapshot) astCacheData(ctx context.Context, spkg source.Package, pos token.Pos) (*astCacheData, error) { - pkg := spkg.(*pkg) - pkgHandle := s.getPackage(pkg.m.ID, pkg.mode) - if pkgHandle == nil { - return nil, fmt.Errorf("could not reconstruct package handle for %v", pkg.m.ID) - } - tok := s.FileSet().File(pos) - if tok == nil { - return nil, fmt.Errorf("no file for pos %v", pos) - } - pgf, err := pkg.File(span.URIFromPath(tok.Name())) - if err != nil { - return nil, err + s.mu.Lock() + entry, hit := s.parsedGoFiles.Get(key) + s.mu.Unlock() + + // cache miss? + if !hit { + handle, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} { + parsed, err := parseGoImpl(ctx, arg.(*snapshot).FileSet(), fh, mode) + return parseGoResult{parsed, err} + }) + + s.mu.Lock() + // Check cache again in case another thread got there first. + if prev, ok := s.parsedGoFiles.Get(key); ok { + entry = prev + release() + } else { + entry = handle + s.parsedGoFiles.Set(key, entry, func(_, _ interface{}) { release() }) + } + s.mu.Unlock() } - astHandle := s.generation.Bind(astCacheKey{pkgHandle.key, pgf.URI}, func(ctx context.Context, arg memoize.Arg) interface{} { - return buildASTCache(pgf) - }, nil) - d, err := astHandle.Get(ctx, s.generation, s) + // Await result. + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) if err != nil { return nil, err } - data := d.(*astCacheData) - if data.err != nil { - return nil, data.err - } - return data, nil + res := v.(parseGoResult) + return res.parsed, res.err } -func (s *snapshot) PosToDecl(ctx context.Context, spkg source.Package, pos token.Pos) (ast.Decl, error) { - data, err := s.astCacheData(ctx, spkg, pos) - if err != nil { - return nil, err +// peekParseGoLocked peeks at the cache used by ParseGo but does not +// populate it or wait for other threads to do so. On cache hit, it returns +// the cache result of parseGoImpl; otherwise it returns (nil, nil). +func (s *snapshot) peekParseGoLocked(fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { + entry, hit := s.parsedGoFiles.Get(parseKey{fh.FileIdentity(), mode}) + if !hit { + return nil, nil // no-one has requested this file } - return data.posToDecl[pos], nil -} - -func (s *snapshot) PosToField(ctx context.Context, spkg source.Package, pos token.Pos) (*ast.Field, error) { - data, err := s.astCacheData(ctx, spkg, pos) - if err != nil { - return nil, err + v := entry.(*memoize.Promise).Cached() + if v == nil { + return nil, nil // parsing is still in progress } - return data.posToField[pos], nil + res := v.(parseGoResult) + return res.parsed, res.err } -type astCacheData struct { - err error - - posToDecl map[token.Pos]ast.Decl - posToField map[token.Pos]*ast.Field -} - -// buildASTCache builds caches to aid in quickly going from the typed -// world to the syntactic world. -func buildASTCache(pgf *source.ParsedGoFile) *astCacheData { - var ( - // path contains all ancestors, including n. - path []ast.Node - // decls contains all ancestors that are decls. - decls []ast.Decl - ) - - data := &astCacheData{ - posToDecl: make(map[token.Pos]ast.Decl), - posToField: make(map[token.Pos]*ast.Field), - } - - ast.Inspect(pgf.File, func(n ast.Node) bool { - if n == nil { - lastP := path[len(path)-1] - path = path[:len(path)-1] - if len(decls) > 0 && decls[len(decls)-1] == lastP { - decls = decls[:len(decls)-1] - } - return false - } - - path = append(path, n) - - switch n := n.(type) { - case *ast.Field: - addField := func(f ast.Node) { - if f.Pos().IsValid() { - data.posToField[f.Pos()] = n - if len(decls) > 0 { - data.posToDecl[f.Pos()] = decls[len(decls)-1] - } - } - } - - // Add mapping for *ast.Field itself. This handles embedded - // fields which have no associated *ast.Ident name. - addField(n) - - // Add mapping for each field name since you can have - // multiple names for the same type expression. - for _, name := range n.Names { - addField(name) - } - - // Also map "X" in "...X" to the containing *ast.Field. This - // makes it easy to format variadic signature params - // properly. - if elips, ok := n.Type.(*ast.Ellipsis); ok && elips.Elt != nil { - addField(elips.Elt) - } - case *ast.FuncDecl: - decls = append(decls, n) - - if n.Name != nil && n.Name.Pos().IsValid() { - data.posToDecl[n.Name.Pos()] = n - } - case *ast.GenDecl: - decls = append(decls, n) - - for _, spec := range n.Specs { - switch spec := spec.(type) { - case *ast.TypeSpec: - if spec.Name != nil && spec.Name.Pos().IsValid() { - data.posToDecl[spec.Name.Pos()] = n - } - case *ast.ValueSpec: - for _, id := range spec.Names { - if id != nil && id.Pos().IsValid() { - data.posToDecl[id.Pos()] = n - } - } - } - } - } - - return true - }) - - return data +// parseGoResult holds the result of a call to parseGoImpl. +type parseGoResult struct { + parsed *source.ParsedGoFile + err error } -func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) *parseGoData { +// parseGoImpl parses the Go source file whose content is provided by fh. +func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { ctx, done := event.Start(ctx, "cache.parseGo", tag.File.Of(fh.URI().Filename())) defer done() ext := filepath.Ext(fh.URI().Filename()) if ext != ".go" && ext != "" { // files generated by cgo have no extension - return &parseGoData{err: fmt.Errorf("cannot parse non-Go file %s", fh.URI())} + return nil, fmt.Errorf("cannot parse non-Go file %s", fh.URI()) } src, err := fh.Read() if err != nil { - return &parseGoData{err: err} + return nil, err } parserMode := parser.AllErrors | parser.ParseComments @@ -278,7 +133,7 @@ func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mod tok := fset.File(file.Pos()) if tok == nil { - // file.Pos is the location of the package declaration. If there was + // file.Pos is the location of the package declaration (issue #53202). If there was // none, we can't find the token.File that ParseFile created, and we // have no choice but to recreate it. tok = fset.AddFile(fh.URI().Filename(), -1, len(src)) @@ -302,13 +157,8 @@ func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mod // it is likely we got stuck in a loop somehow. Log out a diff // of the last changes we made to aid in debugging. if i == 9 { - edits, err := myers.ComputeEdits(fh.URI(), string(src), string(newSrc)) - if err != nil { - event.Error(ctx, "error generating fixSrc diff", err, tag.File.Of(tok.Name())) - } else { - unified := diff.ToUnified("before", "after", string(src), edits) - event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) - } + unified := diff.Unified("before", "after", string(src), string(newSrc)) + event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) } newFile, _ := parser.ParseFile(fset, fh.URI().Filename(), newSrc, parserMode) @@ -323,22 +173,20 @@ func parseGo(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mod } } - return &parseGoData{ - parsed: &source.ParsedGoFile{ - URI: fh.URI(), - Mode: mode, - Src: src, - File: file, - Tok: tok, - Mapper: &protocol.ColumnMapper{ - URI: fh.URI(), - TokFile: tok, - Content: src, - }, - ParseErr: parseErr, + return &source.ParsedGoFile{ + URI: fh.URI(), + Mode: mode, + Src: src, + Fixed: fixed, + File: file, + Tok: tok, + Mapper: &protocol.ColumnMapper{ + URI: fh.URI(), + TokFile: tok, + Content: src, }, - fixed: fixed, - } + ParseErr: parseErr, + }, nil } // An unexportedFilter removes as much unexported AST from a set of Files as possible. @@ -383,7 +231,7 @@ func (f *unexportedFilter) keep(ident *ast.Ident) bool { func (f *unexportedFilter) filterDecl(decl ast.Decl) bool { switch decl := decl.(type) { case *ast.FuncDecl: - if ident := recvIdent(decl); ident != nil && !f.keep(ident) { + if ident := source.RecvIdent(decl.Recv); ident != nil && !f.keep(ident) { return false } return f.keep(decl.Name) @@ -425,6 +273,8 @@ func (f *unexportedFilter) filterSpec(spec ast.Spec) bool { } switch typ := spec.Type.(type) { case *ast.StructType: + // In practice this no longer filters anything; + // see comment at StructType case in recordUses. f.filterFieldList(typ.Fields) case *ast.InterfaceType: f.filterFieldList(typ.Methods) @@ -456,7 +306,7 @@ func (f *unexportedFilter) recordUses(file *ast.File) { switch decl := decl.(type) { case *ast.FuncDecl: // Ignore methods on dropped types. - if ident := recvIdent(decl); ident != nil && !f.keep(ident) { + if ident := source.RecvIdent(decl.Recv); ident != nil && !f.keep(ident) { break } // Ignore functions with dropped names. @@ -480,9 +330,19 @@ func (f *unexportedFilter) recordUses(file *ast.File) { case *ast.TypeSpec: switch typ := spec.Type.(type) { case *ast.StructType: - f.recordFieldUses(false, typ.Fields) + // We used to trim unexported fields but this + // had observable consequences. For example, + // the 'fieldalignment' analyzer would compute + // incorrect diagnostics from the size and + // offsets, and the UI hover information for + // types was inaccurate. So now we keep them. + if typ.Fields != nil { + for _, field := range typ.Fields.List { + f.recordIdents(field.Type) + } + } case *ast.InterfaceType: - f.recordFieldUses(false, typ.Methods) + f.recordInterfaceMethodUses(typ.Methods) } } } @@ -490,21 +350,6 @@ func (f *unexportedFilter) recordUses(file *ast.File) { } } -// recvIdent returns the identifier of a method receiver, e.g. *int. -func recvIdent(decl *ast.FuncDecl) *ast.Ident { - if decl.Recv == nil || len(decl.Recv.List) == 0 { - return nil - } - x := decl.Recv.List[0].Type - if star, ok := x.(*ast.StarExpr); ok { - x = star.X - } - if ident, ok := x.(*ast.Ident); ok { - return ident - } - return nil -} - // recordIdents records unexported identifiers in an Expr in uses. // These may be types, e.g. in map[key]value, function names, e.g. in foo(), // or simple variable references. References that will be discarded, such @@ -531,37 +376,32 @@ func (f *unexportedFilter) recordIdents(x ast.Expr) { } // recordFuncType records the types mentioned by a function type. -func (f *unexportedFilter) recordFuncType(x *ast.FuncType) { - f.recordFieldUses(true, x.Params) - f.recordFieldUses(true, x.Results) -} - -// recordFieldUses records unexported identifiers used in fields, which may be -// struct members, interface members, or function parameter/results. -func (f *unexportedFilter) recordFieldUses(isParams bool, fields *ast.FieldList) { - if fields == nil { - return - } - for _, field := range fields.List { - if isParams { - // Parameter types of retained functions need to be retained. +func (f *unexportedFilter) recordFuncType(fn *ast.FuncType) { + // Parameter and result types of retained functions need to be retained. + if fn.Params != nil { + for _, field := range fn.Params.List { f.recordIdents(field.Type) - continue } - if ft, ok := field.Type.(*ast.FuncType); ok { - // Function declarations in interfaces need all their types retained. - f.recordFuncType(ft) - continue - } - if len(field.Names) == 0 { - // Embedded fields might contribute exported names. + } + if fn.Results != nil { + for _, field := range fn.Results.List { f.recordIdents(field.Type) } - for _, name := range field.Names { - // We only need normal fields if they're exported. - if ast.IsExported(name.Name) { - f.recordIdents(field.Type) - break + } +} + +// recordInterfaceMethodUses records unexported identifiers used in interface methods. +func (f *unexportedFilter) recordInterfaceMethodUses(methods *ast.FieldList) { + if methods != nil { + for _, method := range methods.List { + if len(method.Names) == 0 { + // I, pkg.I, I[T] -- embedded interface: + // may contribute exported names. + f.recordIdents(method.Type) + } else if ft, ok := method.Type.(*ast.FuncType); ok { + // f(T) -- ordinary interface method: + // needs all its types retained. + f.recordFuncType(ft) } } } @@ -588,32 +428,35 @@ func (f *unexportedFilter) ProcessErrors(errors []types.Error) (map[string]bool, } // trimAST clears any part of the AST not relevant to type checking -// expressions at pos. +// the package-level declarations. func trimAST(file *ast.File) { - ast.Inspect(file, func(n ast.Node) bool { - if n == nil { - return false + // Eliminate bodies of top-level functions, methods, inits. + for _, decl := range file.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok { + fn.Body = nil } + } + + // Simplify remaining declarations. + ast.Inspect(file, func(n ast.Node) bool { switch n := n.(type) { - case *ast.FuncDecl: - n.Body = nil - case *ast.BlockStmt: - n.List = nil - case *ast.CaseClause: - n.Body = nil - case *ast.CommClause: - n.Body = nil + case *ast.FuncLit: + // Eliminate bodies of literal functions. + // func() { ... } => func() {} + n.Body.List = nil case *ast.CompositeLit: // types.Info.Types for long slice/array literals are particularly - // expensive. Try to clear them out. + // expensive. Try to clear them out: T{e, ..., e} => T{} at, ok := n.Type.(*ast.ArrayType) if !ok { - // Composite literal. No harm removing all its fields. + // Map or struct literal: no harm removing all its fields. n.Elts = nil break } + // Removing the elements from an ellipsis array changes its type. // Try to set the length explicitly so we can continue. + // [...]T{e, ..., e} => [3]T[]{} if _, ok := at.Len.(*ast.Ellipsis); ok { length, ok := arrayLength(n) if !ok { diff --git a/internal/lsp/cache/parse_test.go b/gopls/internal/lsp/cache/parse_test.go similarity index 98% rename from internal/lsp/cache/parse_test.go rename to gopls/internal/lsp/cache/parse_test.go index cb620f27432..e8db64530e6 100644 --- a/internal/lsp/cache/parse_test.go +++ b/gopls/internal/lsp/cache/parse_test.go @@ -149,7 +149,7 @@ type Exported struct { } var Var = Exported{foo:1} `, - kept: []string{"Exported", "Var"}, + kept: []string{"Exported", "Var", "x"}, }, { name: "drop_function_literals", diff --git a/internal/lsp/cache/parsemode_go116.go b/gopls/internal/lsp/cache/parsemode_go116.go similarity index 100% rename from internal/lsp/cache/parsemode_go116.go rename to gopls/internal/lsp/cache/parsemode_go116.go diff --git a/internal/lsp/cache/parsemode_go117.go b/gopls/internal/lsp/cache/parsemode_go117.go similarity index 100% rename from internal/lsp/cache/parsemode_go117.go rename to gopls/internal/lsp/cache/parsemode_go117.go diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go new file mode 100644 index 00000000000..ddfb9ea7e96 --- /dev/null +++ b/gopls/internal/lsp/cache/pkg.go @@ -0,0 +1,193 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cache + +import ( + "fmt" + "go/ast" + "go/scanner" + "go/types" + "sort" + + "golang.org/x/mod/module" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/memoize" +) + +// pkg contains the type information needed by the source package. +type pkg struct { + m *Metadata + mode source.ParseMode + goFiles []*source.ParsedGoFile + compiledGoFiles []*source.ParsedGoFile + diagnostics []*source.Diagnostic + deps map[PackageID]*pkg // use m.DepsBy{Pkg,Imp}Path to look up ID + version *module.Version + parseErrors []scanner.ErrorList + typeErrors []types.Error + types *types.Package + typesInfo *types.Info + typesSizes types.Sizes + hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors + + analyses memoize.Store // maps analyzer.Name to Promise[actionResult] +} + +func (p *pkg) String() string { return p.ID() } + +// A loadScope defines a package loading scope for use with go/packages. +type loadScope interface { + aScope() +} + +type ( + fileLoadScope span.URI // load packages containing a file (including command-line-arguments) + packageLoadScope string // load a specific package + moduleLoadScope string // load packages in a specific module + viewLoadScope span.URI // load the workspace +) + +// Implement the loadScope interface. +func (fileLoadScope) aScope() {} +func (packageLoadScope) aScope() {} +func (moduleLoadScope) aScope() {} +func (viewLoadScope) aScope() {} + +func (p *pkg) ID() string { + return string(p.m.ID) +} + +func (p *pkg) Name() string { + return string(p.m.Name) +} + +func (p *pkg) PkgPath() string { + return string(p.m.PkgPath) +} + +func (p *pkg) ParseMode() source.ParseMode { + return p.mode +} + +func (p *pkg) CompiledGoFiles() []*source.ParsedGoFile { + return p.compiledGoFiles +} + +func (p *pkg) File(uri span.URI) (*source.ParsedGoFile, error) { + for _, cgf := range p.compiledGoFiles { + if cgf.URI == uri { + return cgf, nil + } + } + for _, gf := range p.goFiles { + if gf.URI == uri { + return gf, nil + } + } + return nil, fmt.Errorf("no parsed file for %s in %v", uri, p.m.ID) +} + +func (p *pkg) GetSyntax() []*ast.File { + var syntax []*ast.File + for _, pgf := range p.compiledGoFiles { + syntax = append(syntax, pgf.File) + } + return syntax +} + +func (p *pkg) GetTypes() *types.Package { + return p.types +} + +func (p *pkg) GetTypesInfo() *types.Info { + return p.typesInfo +} + +func (p *pkg) GetTypesSizes() types.Sizes { + return p.typesSizes +} + +func (p *pkg) ForTest() string { + return string(p.m.ForTest) +} + +// DirectDep returns the directly imported dependency of this package, +// given its PackagePath. (If you have an ImportPath, e.g. a string +// from an import declaration, use ResolveImportPath instead. +// They may differ in case of vendoring.) +func (p *pkg) DirectDep(pkgPath string) (source.Package, error) { + if id, ok := p.m.DepsByPkgPath[PackagePath(pkgPath)]; ok { + if imp := p.deps[id]; imp != nil { + return imp, nil + } + } + return nil, fmt.Errorf("package does not import package with path %s", pkgPath) +} + +// ResolveImportPath returns the directly imported dependency of this package, +// given its ImportPath. See also DirectDep. +func (p *pkg) ResolveImportPath(importPath string) (source.Package, error) { + if id, ok := p.m.DepsByImpPath[ImportPath(importPath)]; ok && id != "" { + if imp := p.deps[id]; imp != nil { + return imp, nil + } + } + return nil, fmt.Errorf("package does not import %s", importPath) +} + +func (p *pkg) MissingDependencies() []string { + // We don't invalidate metadata for import deletions, + // so check the package imports via the *types.Package. + // + // rfindley says: it looks like this is intending to implement + // a heuristic "if go list couldn't resolve import paths to + // packages, then probably you're not in GOPATH or a module". + // This is used to determine if we need to show a warning diagnostic. + // It looks like this logic is implementing the heuristic that + // "even if the metadata has a MissingDep, if the types.Package + // doesn't need that dep anymore we shouldn't show the warning". + // But either we're outside of GOPATH/Module, or we're not... + // + // adonovan says: I think this effectively reverses the + // heuristic used by the type checker when Importer.Import + // returns an error---go/types synthesizes a package whose + // Path is the import path (sans "vendor/")---hence the + // dubious ImportPath() conversion. A blank DepsByImpPath + // entry means a missing import. + // + // If we invalidate the metadata for import deletions (which + // should be fast) then we can simply return the blank entries + // in DepsByImpPath. (They are PackageIDs not PackagePaths, + // but the caller only cares whether the set is empty!) + var missing []string + for _, pkg := range p.types.Imports() { + if id, ok := p.m.DepsByImpPath[ImportPath(pkg.Path())]; ok && id == "" { + missing = append(missing, pkg.Path()) + } + } + sort.Strings(missing) + return missing +} + +func (p *pkg) Imports() []source.Package { + var result []source.Package // unordered + for _, dep := range p.deps { + result = append(result, dep) + } + return result +} + +func (p *pkg) Version() *module.Version { + return p.version +} + +func (p *pkg) HasListOrParseErrors() bool { + return len(p.m.Errors) != 0 || len(p.parseErrors) != 0 +} + +func (p *pkg) HasTypeErrors() bool { + return len(p.typeErrors) != 0 +} diff --git a/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go similarity index 76% rename from internal/lsp/cache/session.go rename to gopls/internal/lsp/cache/session.go index e018cb33bd8..57bba47df80 100644 --- a/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -7,16 +7,20 @@ package cache import ( "context" "fmt" + "os" "strconv" + "strings" "sync" "sync/atomic" + "golang.org/x/tools/gopls/internal/govulncheck" + "golang.org/x/tools/gopls/internal/lsp/progress" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/progress" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/xcontext" ) @@ -44,7 +48,7 @@ type overlay struct { session *Session uri span.URI text []byte - hash string + hash source.Hash version int32 kind source.FileKind @@ -117,26 +121,31 @@ func (c *closedFile) Version() int32 { return 0 } +// ID returns the unique identifier for this session on this server. func (s *Session) ID() string { return s.id } func (s *Session) String() string { return s.id } +// Options returns a copy of the SessionOptions for this session. func (s *Session) Options() *source.Options { s.optionsMu.Lock() defer s.optionsMu.Unlock() return s.options } +// SetOptions sets the options of this session to new values. func (s *Session) SetOptions(options *source.Options) { s.optionsMu.Lock() defer s.optionsMu.Unlock() s.options = options } +// SetProgressTracker sets the progress tracker for the session. func (s *Session) SetProgressTracker(tracker *progress.Tracker) { // The progress tracker should be set before any view is initialized. s.progress = tracker } +// Shutdown the session and all views it has created. func (s *Session) Shutdown(ctx context.Context) { var views []*View s.viewMu.Lock() @@ -150,10 +159,16 @@ func (s *Session) Shutdown(ctx context.Context) { event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s)) } -func (s *Session) Cache() interface{} { +// Cache returns the cache that created this session, for debugging only. +func (s *Session) Cache() *Cache { return s.cache } +// NewView creates a new View, returning it and its first snapshot. If a +// non-empty tempWorkspace directory is provided, the View will record a copy +// of its gopls workspace module in that directory, so that client tooling +// can execute in the same main module. On success it also returns a release +// function that must be called when the Snapshot is no longer needed. func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (source.View, source.Snapshot, func(), error) { s.viewMu.Lock() defer s.viewMu.Unlock() @@ -164,7 +179,7 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt } view, snapshot, release, err := s.createView(ctx, name, folder, options, 0) if err != nil { - return nil, nil, func() {}, err + return nil, nil, nil, err } s.views = append(s.views, view) // we always need to drop the view map @@ -179,21 +194,32 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, s.cache.options(options) } - // Set the module-specific information. - ws, err := s.getWorkspaceInformation(ctx, folder, options) + // Get immutable workspace configuration. + // + // TODO(rfindley): this info isn't actually immutable. For example, GOWORK + // could be changed, or a user's environment could be modified. + // We need a mechanism to invalidate it. + wsInfo, err := s.getWorkspaceInformation(ctx, folder, options) if err != nil { return nil, nil, func() {}, err } + root := folder if options.ExpandWorkspaceToModule { - root, err = findWorkspaceRoot(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), ws.gomodcache, options), options.ExperimentalWorkspaceModule) + root, err = findWorkspaceRoot(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err } } + explicitGowork := os.Getenv("GOWORK") + if v, ok := options.Env["GOWORK"]; ok { + explicitGowork = v + } + goworkURI := span.URIFromPath(explicitGowork) + // Build the gopls workspace, collecting active modules in the view. - workspace, err := newWorkspace(ctx, root, s, pathExcludedByFilterFunc(root.Filename(), ws.gomodcache, options), ws.userGo111Module == off, options.ExperimentalWorkspaceModule) + workspace, err := newWorkspace(ctx, root, goworkURI, s, pathExcludedByFilterFunc(root.Filename(), wsInfo.gomodcache, options), wsInfo.userGo111Module == off, options.ExperimentalWorkspaceModule) if err != nil { return nil, nil, func() {}, err } @@ -212,53 +238,74 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, baseCtx: baseCtx, name: name, folder: folder, - moduleUpgrades: map[string]string{}, + moduleUpgrades: map[span.URI]map[string]string{}, + vulns: map[span.URI][]govulncheck.Vuln{}, filesByURI: map[span.URI]*fileBase{}, filesByBase: map[string][]*fileBase{}, rootURI: root, - workspaceInformation: *ws, + explicitGowork: goworkURI, + workspaceInformation: *wsInfo, } v.importsState = &importsState{ ctx: backgroundCtx, processEnv: &imports.ProcessEnv{ GocmdRunner: s.gocmdRunner, + SkipPathInScan: func(dir string) bool { + prefix := strings.TrimSuffix(string(v.folder), "/") + "/" + uri := strings.TrimSuffix(string(span.URIFromPath(dir)), "/") + if !strings.HasPrefix(uri+"/", prefix) { + return false + } + filterer := source.NewFilterer(options.DirectoryFilters) + rel := strings.TrimPrefix(uri, prefix) + disallow := filterer.Disallow(rel) + return disallow + }, }, } v.snapshot = &snapshot{ - id: snapshotID, - view: v, - backgroundCtx: backgroundCtx, - cancel: cancel, - initializeOnce: &sync.Once{}, - generation: s.cache.store.Generation(generationName(v, 0)), - packages: make(map[packageKey]*packageHandle), - meta: NewMetadataGraph(), - files: make(map[span.URI]source.VersionedFileHandle), - goFiles: make(map[parseKey]*parseGoHandle), - symbols: make(map[span.URI]*symbolHandle), - actions: make(map[actionKey]*actionHandle), - workspacePackages: make(map[PackageID]PackagePath), - unloadableFiles: make(map[span.URI]struct{}), - parseModHandles: make(map[span.URI]*parseModHandle), - parseWorkHandles: make(map[span.URI]*parseWorkHandle), - modTidyHandles: make(map[span.URI]*modTidyHandle), - modWhyHandles: make(map[span.URI]*modWhyHandle), - workspace: workspace, - } + id: snapshotID, + view: v, + backgroundCtx: backgroundCtx, + cancel: cancel, + store: s.cache.store, + packages: persistent.NewMap(packageKeyLessInterface), + meta: &metadataGraph{}, + files: newFilesMap(), + isActivePackageCache: newIsActivePackageCacheMap(), + parsedGoFiles: persistent.NewMap(parseKeyLessInterface), + parseKeysByURI: newParseKeysByURIMap(), + symbolizeHandles: persistent.NewMap(uriLessInterface), + actions: persistent.NewMap(actionKeyLessInterface), + workspacePackages: make(map[PackageID]PackagePath), + unloadableFiles: make(map[span.URI]struct{}), + parseModHandles: persistent.NewMap(uriLessInterface), + parseWorkHandles: persistent.NewMap(uriLessInterface), + modTidyHandles: persistent.NewMap(uriLessInterface), + modWhyHandles: persistent.NewMap(uriLessInterface), + knownSubdirs: newKnownDirsSet(), + workspace: workspace, + } + // Save one reference in the view. + v.releaseSnapshot = v.snapshot.Acquire() // Initialize the view without blocking. initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx)) v.initCancelFirstAttempt = initCancel snapshot := v.snapshot - release := snapshot.generation.Acquire() + + // Pass a second reference to the background goroutine. + bgRelease := snapshot.Acquire() go func() { - defer release() + defer bgRelease() snapshot.initialize(initCtx, true) }() - return v, snapshot, snapshot.generation.Acquire(), nil + + // Return a third reference to the caller. + return v, snapshot, snapshot.Acquire(), nil } -// View returns the view by name. +// View returns a view with a matching name, if the session has one. func (s *Session) View(name string) source.View { s.viewMu.RLock() defer s.viewMu.RUnlock() @@ -291,19 +338,6 @@ func (s *Session) viewOf(uri span.URI) (*View, error) { return s.viewMap[uri], nil } -func (s *Session) viewsOf(uri span.URI) []*View { - s.viewMu.RLock() - defer s.viewMu.RUnlock() - - var views []*View - for _, view := range s.views { - if source.InDir(view.folder.Filename(), uri.Filename()) { - views = append(views, view) - } - } - return views -} - func (s *Session) Views() []source.View { s.viewMu.RLock() defer s.viewMu.RUnlock() @@ -323,6 +357,8 @@ func bestViewForURI(uri span.URI, views []*View) *View { if longest != nil && len(longest.Folder()) > len(view.Folder()) { continue } + // TODO(rfindley): this should consider the workspace layout (i.e. + // go.work). if view.contains(uri) { longest = view } @@ -405,13 +441,13 @@ func (s *Session) dropView(ctx context.Context, v *View) (int, error) { } func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModification) error { - _, releases, err := s.DidModifyFiles(ctx, changes) - for _, release := range releases { - release() - } + _, release, err := s.DidModifyFiles(ctx, changes) + release() return err } +// TODO(rfindley): fileChange seems redundant with source.FileModification. +// De-dupe into a common representation for changes. type fileChange struct { content []byte exists bool @@ -423,7 +459,12 @@ type fileChange struct { isUnchanged bool } -func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, []func(), error) { +// DidModifyFiles reports a file modification to the session. It returns +// the new snapshots after the modifications have been applied, paired with +// the affected file URIs for those snapshots. +// On success, it returns a release function that +// must be called when the snapshots are no longer needed. +func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, func(), error) { s.viewMu.RLock() defer s.viewMu.RUnlock() views := make(map[*View]map[span.URI]*fileChange) @@ -504,6 +545,14 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif viewToSnapshot[view] = snapshot } + // The release function is called when the + // returned URIs no longer need to be valid. + release := func() { + for _, release := range releases { + release() + } + } + // We only want to diagnose each changed file once, in the view to which // it "most" belongs. We do this by picking the best view for each URI, // and then aggregating the set of snapshots and their URIs (to avoid @@ -521,9 +570,13 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif } snapshotURIs[snapshot] = append(snapshotURIs[snapshot], mod.URI) } - return snapshotURIs, releases, nil + + return snapshotURIs, release, nil } +// ExpandModificationsToDirectories returns the set of changes with the +// directory changes removed and expanded to include all of the files in +// the directory. func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification { s.viewMu.RLock() defer s.viewMu.RUnlock() @@ -533,10 +586,14 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes defer release() snapshots = append(snapshots, snapshot) } + // TODO(adonovan): opt: release lock here. + knownDirs := knownDirectories(ctx, snapshots) + defer knownDirs.Destroy() + var result []source.FileModification for _, c := range changes { - if _, ok := knownDirs[c.URI]; !ok { + if !knownDirs.Contains(c.URI) { result = append(result, c) continue } @@ -558,16 +615,17 @@ func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes // knownDirectories returns all of the directories known to the given // snapshots, including workspace directories and their subdirectories. -func knownDirectories(ctx context.Context, snapshots []*snapshot) map[span.URI]struct{} { - result := map[span.URI]struct{}{} +// It is responsibility of the caller to destroy the returned set. +func knownDirectories(ctx context.Context, snapshots []*snapshot) knownDirsSet { + result := newKnownDirsSet() for _, snapshot := range snapshots { dirs := snapshot.workspace.dirs(ctx, snapshot) for _, dir := range dirs { - result[dir] = struct{}{} - } - for _, dir := range snapshot.getKnownSubdirs(dirs) { - result[dir] = struct{}{} + result.Insert(dir) } + knownSubdirs := snapshot.getKnownSubdirs(dirs) + result.SetAll(knownSubdirs) + knownSubdirs.Destroy() } return result } @@ -637,7 +695,7 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif if c.OnDisk || c.Action == source.Save { version = o.version } - hash := hashContents(text) + hash := source.HashOf(text) var sameContentOnDisk bool switch c.Action { case source.Delete: @@ -694,6 +752,7 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif return overlays, nil } +// GetFile returns a handle for the specified file. func (s *Session) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { if overlay := s.readOverlay(uri); overlay != nil { return overlay, nil @@ -712,6 +771,7 @@ func (s *Session) readOverlay(uri span.URI) *overlay { return nil } +// Overlays returns a slice of file overlays for the session. func (s *Session) Overlays() []source.Overlay { s.overlayMu.Lock() defer s.overlayMu.Unlock() @@ -723,6 +783,9 @@ func (s *Session) Overlays() []source.Overlay { return overlays } +// FileWatchingGlobPatterns returns glob patterns to watch every directory +// known by the view. For views within a module, this is the module root, +// any directory in the module root, and any replace targets. func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { s.viewMu.RLock() defer s.viewMu.RUnlock() diff --git a/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go similarity index 64% rename from internal/lsp/cache/snapshot.go rename to gopls/internal/lsp/cache/snapshot.go index a219935aa66..b05f401c52a 100644 --- a/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -14,49 +14,52 @@ import ( "go/types" "io" "io/ioutil" + "log" "os" "path/filepath" "regexp" + "runtime" "sort" "strconv" "strings" "sync" + "sync/atomic" + "unsafe" "golang.org/x/mod/modfile" "golang.org/x/mod/module" "golang.org/x/mod/semver" - "golang.org/x/tools/go/analysis" + "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/debug/log" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/packagesinternal" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/typesinternal" ) type snapshot struct { - memoize.Arg // allow as a memoize.Function arg - id uint64 view *View cancel func() backgroundCtx context.Context - // the cache generation that contains the data for this snapshot. - generation *memoize.Generation + store *memoize.Store // cache of handles shared by all snapshots - // The snapshot's initialization state is controlled by the fields below. - // - // initializeOnce guards snapshot initialization. Each snapshot is - // initialized at most once: reinitialization is triggered on later snapshots - // by invalidating this field. - initializeOnce *sync.Once + refcount sync.WaitGroup // number of references + destroyedBy *string // atomically set to non-nil in Destroy once refcount = 0 + + // initialized reports whether the snapshot has been initialized. Concurrent + // initialization is guarded by the view.initializationSema. Each snapshot is + // initialized at most once: concurrent initialization is guarded by + // view.initializationSema. + initialized bool // initializedErr holds the last error resulting from initialization. If // initialization fails, we only retry when the the workspace modules change, // to avoid too many go/packages calls. @@ -69,66 +72,164 @@ type snapshot struct { builtin span.URI // meta holds loaded metadata. + // + // meta is guarded by mu, but the metadataGraph itself is immutable. + // TODO(rfindley): in many places we hold mu while operating on meta, even + // though we only need to hold mu while reading the pointer. meta *metadataGraph // files maps file URIs to their corresponding FileHandles. // It may invalidated when a file's content changes. - files map[span.URI]source.VersionedFileHandle + files filesMap - // goFiles maps a parseKey to its parseGoHandle. - goFiles map[parseKey]*parseGoHandle + // parsedGoFiles maps a parseKey to the handle of the future result of parsing it. + parsedGoFiles *persistent.Map // from parseKey to *memoize.Promise[parseGoResult] - // TODO(rfindley): consider merging this with files to reduce burden on clone. - symbols map[span.URI]*symbolHandle + // parseKeysByURI records the set of keys of parsedGoFiles that + // need to be invalidated for each URI. + // TODO(adonovan): opt: parseKey = ParseMode + URI, so this could + // be just a set of ParseModes, or we could loop over AllParseModes. + parseKeysByURI parseKeysByURIMap - // packages maps a packageKey to a set of packageHandles to which that file belongs. + // symbolizeHandles maps each file URI to a handle for the future + // result of computing the symbols declared in that file. + symbolizeHandles *persistent.Map // from span.URI to *memoize.Promise[symbolizeResult] + + // packages maps a packageKey to a *packageHandle. // It may be invalidated when a file's content changes. - packages map[packageKey]*packageHandle + // + // Invariants to preserve: + // - packages.Get(id).m.Metadata == meta.metadata[id].Metadata for all ids + // - if a package is in packages, then all of its dependencies should also + // be in packages, unless there is a missing import + packages *persistent.Map // from packageKey to *memoize.Promise[*packageHandle] - // actions maps an actionkey to its actionHandle. - actions map[actionKey]*actionHandle + // isActivePackageCache maps package ID to the cached value if it is active or not. + // It may be invalidated when metadata changes or a new file is opened or closed. + isActivePackageCache isActivePackageCacheMap + + // actions maps an actionKey to the handle for the future + // result of execution an analysis pass on a package. + actions *persistent.Map // from actionKey to *actionHandle // workspacePackages contains the workspace's packages, which are loaded // when the view is created. workspacePackages map[PackageID]PackagePath + // shouldLoad tracks packages that need to be reloaded, mapping a PackageID + // to the package paths that should be used to reload it + // + // When we try to load a package, we clear it from the shouldLoad map + // regardless of whether the load succeeded, to prevent endless loads. + shouldLoad map[PackageID][]PackagePath + // unloadableFiles keeps track of files that we've failed to load. unloadableFiles map[span.URI]struct{} // parseModHandles keeps track of any parseModHandles for the snapshot. // The handles need not refer to only the view's go.mod file. - parseModHandles map[span.URI]*parseModHandle + parseModHandles *persistent.Map // from span.URI to *memoize.Promise[parseModResult] // parseWorkHandles keeps track of any parseWorkHandles for the snapshot. // The handles need not refer to only the view's go.work file. - parseWorkHandles map[span.URI]*parseWorkHandle + parseWorkHandles *persistent.Map // from span.URI to *memoize.Promise[parseWorkResult] // Preserve go.mod-related handles to avoid garbage-collecting the results // of various calls to the go command. The handles need not refer to only // the view's go.mod file. - modTidyHandles map[span.URI]*modTidyHandle - modWhyHandles map[span.URI]*modWhyHandle + modTidyHandles *persistent.Map // from span.URI to *memoize.Promise[modTidyResult] + modWhyHandles *persistent.Map // from span.URI to *memoize.Promise[modWhyResult] + + workspace *workspace // (not guarded by mu) - workspace *workspace - workspaceDirHandle *memoize.Handle + // The cached result of makeWorkspaceDir, created on demand and deleted by Snapshot.Destroy. + workspaceDir string + workspaceDirErr error // knownSubdirs is the set of subdirectories in the workspace, used to // create glob patterns for file watching. - knownSubdirs map[span.URI]struct{} + knownSubdirs knownDirsSet + knownSubdirsPatternCache string // unprocessedSubdirChanges are any changes that might affect the set of // subdirectories in the workspace. They are not reflected to knownSubdirs // during the snapshot cloning step as it can slow down cloning. unprocessedSubdirChanges []*fileChange } -type packageKey struct { - mode source.ParseMode - id PackageID +var _ memoize.RefCounted = (*snapshot)(nil) // snapshots are reference-counted + +// Acquire prevents the snapshot from being destroyed until the returned function is called. +// +// (s.Acquire().release() could instead be expressed as a pair of +// method calls s.IncRef(); s.DecRef(). The latter has the advantage +// that the DecRefs are fungible and don't require holding anything in +// addition to the refcounted object s, but paradoxically that is also +// an advantage of the current approach, which forces the caller to +// consider the release function at every stage, making a reference +// leak more obvious.) +func (s *snapshot) Acquire() func() { + type uP = unsafe.Pointer + if destroyedBy := atomic.LoadPointer((*uP)(uP(&s.destroyedBy))); destroyedBy != nil { + log.Panicf("%d: acquire() after Destroy(%q)", s.id, *(*string)(destroyedBy)) + } + s.refcount.Add(1) + return s.refcount.Done } -type actionKey struct { - pkg packageKey - analyzer *analysis.Analyzer +func (s *snapshot) awaitPromise(ctx context.Context, p *memoize.Promise) (interface{}, error) { + return p.Get(ctx, s) +} + +// destroy waits for all leases on the snapshot to expire then releases +// any resources (reference counts and files) associated with it. +// Snapshots being destroyed can be awaited using v.destroyWG. +// +// TODO(adonovan): move this logic into the release function returned +// by Acquire when the reference count becomes zero. (This would cost +// us the destroyedBy debug info, unless we add it to the signature of +// memoize.RefCounted.Acquire.) +// +// The destroyedBy argument is used for debugging. +// +// v.snapshotMu must be held while calling this function, in order to preserve +// the invariants described by the the docstring for v.snapshot. +func (v *View) destroy(s *snapshot, destroyedBy string) { + v.snapshotWG.Add(1) + go func() { + defer v.snapshotWG.Done() + s.destroy(destroyedBy) + }() +} + +func (s *snapshot) destroy(destroyedBy string) { + // Wait for all leases to end before commencing destruction. + s.refcount.Wait() + + // Report bad state as a debugging aid. + // Not foolproof: another thread could acquire() at this moment. + type uP = unsafe.Pointer // looking forward to generics... + if old := atomic.SwapPointer((*uP)(uP(&s.destroyedBy)), uP(&destroyedBy)); old != nil { + log.Panicf("%d: Destroy(%q) after Destroy(%q)", s.id, destroyedBy, *(*string)(old)) + } + + s.packages.Destroy() + s.isActivePackageCache.Destroy() + s.actions.Destroy() + s.files.Destroy() + s.parsedGoFiles.Destroy() + s.parseKeysByURI.Destroy() + s.knownSubdirs.Destroy() + s.symbolizeHandles.Destroy() + s.parseModHandles.Destroy() + s.parseWorkHandles.Destroy() + s.modTidyHandles.Destroy() + s.modWhyHandles.Destroy() + + if s.workspaceDir != "" { + if err := os.RemoveAll(s.workspaceDir); err != nil { + event.Error(context.Background(), "cleaning workspace dir", err) + } + } } func (s *snapshot) ID() uint64 { @@ -149,7 +250,7 @@ func (s *snapshot) FileSet() *token.FileSet { func (s *snapshot) ModFiles() []span.URI { var uris []span.URI - for modURI := range s.workspace.getActiveModFiles() { + for modURI := range s.workspace.ActiveModFiles() { uris = append(uris, modURI) } return uris @@ -164,16 +265,33 @@ func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle { defer s.mu.Unlock() tmpls := map[span.URI]source.VersionedFileHandle{} - for k, fh := range s.files { + s.files.Range(func(k span.URI, fh source.VersionedFileHandle) { if s.view.FileKind(fh) == source.Tmpl { tmpls[k] = fh } - } + }) return tmpls } func (s *snapshot) ValidBuildConfiguration() bool { - return validBuildConfiguration(s.view.rootURI, &s.view.workspaceInformation, s.workspace.getActiveModFiles()) + // Since we only really understand the `go` command, if the user has a + // different GOPACKAGESDRIVER, assume that their configuration is valid. + if s.view.hasGopackagesDriver { + return true + } + // Check if the user is working within a module or if we have found + // multiple modules in the workspace. + if len(s.workspace.ActiveModFiles()) > 0 { + return true + } + // The user may have a multiple directories in their GOPATH. + // Check if the workspace is within any of them. + for _, gp := range filepath.SplitList(s.view.gopath) { + if source.InDir(filepath.Join(gp, "src"), s.view.rootURI.Filename()) { + return true + } + } + return false } // workspaceMode describes the way in which the snapshot's workspace should @@ -190,7 +308,7 @@ func (s *snapshot) workspaceMode() workspaceMode { // If the view is not in a module and contains no modules, but still has a // valid workspace configuration, do not create the workspace module. // It could be using GOPATH or a different build system entirely. - if len(s.workspace.getActiveModFiles()) == 0 && validBuildConfiguration { + if len(s.workspace.ActiveModFiles()) == 0 && validBuildConfiguration { return mode } mode |= moduleMode @@ -306,10 +424,24 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri return true, modBytes, sumBytes, nil } +// goCommandInvocation populates inv with configuration for running go commands on the snapshot. +// +// TODO(rfindley): refactor this function to compose the required configuration +// explicitly, rather than implicitly deriving it from flags and inv. +// +// TODO(adonovan): simplify cleanup mechanism. It's hard to see, but +// it used only after call to tempModFile. Clarify that it is only +// non-nil on success. func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.InvocationFlags, inv *gocommand.Invocation) (tmpURI span.URI, updatedInv *gocommand.Invocation, cleanup func(), err error) { s.view.optionsMu.Lock() allowModfileModificationOption := s.view.options.AllowModfileModifications allowNetworkOption := s.view.options.AllowImplicitNetworkAccess + + // TODO(rfindley): this is very hard to follow, and may not even be doing the + // right thing: should inv.Env really trample view.options? Do we ever invoke + // this with a non-empty inv.Env? + // + // We should refactor to make it clearer that the correct env is being used. inv.Env = append(append(append(os.Environ(), s.view.options.EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.effectiveGo111Module) inv.BuildFlags = append([]string{}, s.view.options.BuildFlags...) s.view.optionsMu.Unlock() @@ -334,7 +466,6 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat // - the working directory. // - the -mod flag // - the -modfile flag - // - the -workfile flag // // These are dependent on a number of factors: whether we need to run in a // synthetic workspace, whether flags are supported at the current go @@ -349,7 +480,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat if mode == source.LoadWorkspace { switch s.workspace.moduleSource { case legacyWorkspace: - for m := range s.workspace.getActiveModFiles() { // range to access the only element + for m := range s.workspace.ActiveModFiles() { // range to access the only element modURI = m } case goWorkWorkspace: @@ -385,6 +516,9 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat } } + // TODO(rfindley): in the case of go.work mode, modURI is empty and we fall + // back on the default behavior of vendorEnabled with an empty modURI. Figure + // out what is correct here and implement it explicitly. vendorEnabled, err := s.vendorEnabled(ctx, modURI, modContent) if err != nil { return "", nil, cleanup, err @@ -420,13 +554,15 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return "", nil, cleanup, source.ErrTmpModfileUnsupported } - // We should use -workfile if: - // 1. We're not actively trying to mutate a modfile. - // 2. We have an active go.work file. - // 3. We're using at least Go 1.18. + // We should use -modfile if: + // - the workspace mode supports it + // - we're using a go.work file on go1.18+, or we need a temp mod file (for + // example, if running go mod tidy in a go.work workspace) + // + // TODO(rfindley): this is very hard to follow. Refactor. useWorkFile := !needTempMod && s.workspace.moduleSource == goWorkWorkspace && s.view.goversion >= 18 if useWorkFile { - // TODO(#51215): build a temp workfile and set GOWORK in the environment. + // Since we're running in the workspace root, the go command will resolve GOWORK automatically. } else if useTempMod { if modURI == "" { return "", nil, cleanup, fmt.Errorf("no go.mod file found in %s", inv.WorkingDir) @@ -447,36 +583,44 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat return tmpURI, inv, cleanup, nil } +// usesWorkspaceDir reports whether the snapshot should use a synthetic +// workspace directory for running workspace go commands such as go list. +// +// TODO(rfindley): this logic is duplicated with goCommandInvocation. Clean up +// the latter, and deduplicate. +func (s *snapshot) usesWorkspaceDir() bool { + switch s.workspace.moduleSource { + case legacyWorkspace: + return false + case goWorkWorkspace: + if s.view.goversion >= 18 { + return false + } + // Before go 1.18, the Go command did not natively support go.work files, + // so we 'fake' them with a workspace module. + } + return true +} + func (s *snapshot) buildOverlay() map[string][]byte { s.mu.Lock() defer s.mu.Unlock() overlays := make(map[string][]byte) - for uri, fh := range s.files { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { overlay, ok := fh.(*overlay) if !ok { - continue + return } if overlay.saved { - continue + return } // TODO(rstambler): Make sure not to send overlays outside of the current view. overlays[uri.Filename()] = overlay.text - } + }) return overlays } -func hashUnsavedOverlays(files map[span.URI]source.VersionedFileHandle) string { - var unsaved []string - for uri, fh := range files { - if overlay, ok := fh.(*overlay); ok && !overlay.saved { - unsaved = append(unsaved, uri.Filename()) - } - } - sort.Strings(unsaved) - return hashContents([]byte(strings.Join(unsaved, ""))) -} - func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]source.Package, error) { ctx = event.Label(ctx, tag.URI.Of(uri)) @@ -486,7 +630,7 @@ func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode sourc } var pkgs []source.Package for _, ph := range phs { - pkg, err := ph.check(ctx, s) + pkg, err := ph.await(ctx, s) if err != nil { return nil, err } @@ -524,10 +668,14 @@ func (s *snapshot) PackageForFile(ctx context.Context, uri span.URI, mode source return nil, fmt.Errorf("no packages in input") } - return ph.check(ctx, s) + return ph.await(ctx, s) } -func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]*packageHandle, error) { +func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, withIntermediateTestVariants bool) ([]*packageHandle, error) { + // TODO(rfindley): why can't/shouldn't we awaitLoaded here? It seems that if + // we ask for package handles for a file, we should wait for pending loads. + // Else we will reload orphaned files before the initial load completes. + // Check if we should reload metadata for the file. We don't invalidate IDs // (though we should), so the IDs will be a better source of truth than the // metadata. If there are no IDs for the file, then we should also reload. @@ -547,63 +695,113 @@ func (s *snapshot) packageHandlesForFile(ctx context.Context, uri span.URI, mode for _, id := range knownIDs { // Filter out any intermediate test variants. We typically aren't // interested in these packages for file= style queries. - if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant && !includeTestVariants { + if m := s.getMetadata(id); m != nil && m.IsIntermediateTestVariant() && !withIntermediateTestVariants { continue } - var parseModes []source.ParseMode - switch mode { - case source.TypecheckAll: - if s.workspaceParseMode(id) == source.ParseFull { - parseModes = []source.ParseMode{source.ParseFull} - } else { - parseModes = []source.ParseMode{source.ParseExported, source.ParseFull} - } - case source.TypecheckFull: - parseModes = []source.ParseMode{source.ParseFull} - case source.TypecheckWorkspace: - parseModes = []source.ParseMode{s.workspaceParseMode(id)} + parseMode := source.ParseFull + if mode == source.TypecheckWorkspace { + parseMode = s.workspaceParseMode(id) } - for _, parseMode := range parseModes { - ph, err := s.buildPackageHandle(ctx, id, parseMode) - if err != nil { - return nil, err - } - phs = append(phs, ph) + ph, err := s.buildPackageHandle(ctx, id, parseMode) + if err != nil { + return nil, err } + phs = append(phs, ph) } return phs, nil } +// getOrLoadIDsForURI returns package IDs associated with the file uri. If no +// such packages exist or if they are known to be stale, it reloads the file. +// +// If experimentalUseInvalidMetadata is set, this function may return package +// IDs with invalid metadata. func (s *snapshot) getOrLoadIDsForURI(ctx context.Context, uri span.URI) ([]PackageID, error) { - knownIDs := s.getIDsForURI(uri) - reload := len(knownIDs) == 0 - for _, id := range knownIDs { - // Reload package metadata if any of the metadata has missing - // dependencies, in case something has changed since the last time we - // reloaded it. - if s.noValidMetadataForID(id) { - reload = true - break + useInvalidMetadata := s.useInvalidMetadata() + + s.mu.Lock() + + // Start with the set of package associations derived from the last load. + ids := s.meta.ids[uri] + + hasValidID := false // whether we have any valid package metadata containing uri + shouldLoad := false // whether any packages containing uri are marked 'shouldLoad' + for _, id := range ids { + // TODO(rfindley): remove the defensiveness here. s.meta.metadata[id] must + // exist. + if m, ok := s.meta.metadata[id]; ok && m.Valid { + hasValidID = true + } + if len(s.shouldLoad[id]) > 0 { + shouldLoad = true } - // TODO(golang/go#36918): Previously, we would reload any package with - // missing dependencies. This is expensive and results in too many - // calls to packages.Load. Determine what we should do instead. } - if reload { - err := s.load(ctx, false, fileURI(uri)) - if !s.useInvalidMetadata() && err != nil { - return nil, err + // Check if uri is known to be unloadable. + // + // TODO(rfindley): shouldn't we also mark uri as unloadable if the load below + // fails? Otherwise we endlessly load files with no packages. + _, unloadable := s.unloadableFiles[uri] + + s.mu.Unlock() + + // Special case: if experimentalUseInvalidMetadata is set and we have any + // ids, just return them. + // + // This is arguably wrong: if the metadata is invalid we should try reloading + // it. However, this was the pre-existing behavior, and + // experimentalUseInvalidMetadata will be removed in a future release. + if !shouldLoad && useInvalidMetadata && len(ids) > 0 { + return ids, nil + } + + // Reload if loading is likely to improve the package associations for uri: + // - uri is not contained in any valid packages + // - ...or one of the packages containing uri is marked 'shouldLoad' + // - ...but uri is not unloadable + if (shouldLoad || !hasValidID) && !unloadable { + scope := fileLoadScope(uri) + err := s.load(ctx, false, scope) + + // Guard against failed loads due to context cancellation. + // + // Return the context error here as the current operation is no longer + // valid. + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr } - // We've tried to reload and there are still no known IDs for the URI. - // Return the load error, if there was one. - knownIDs = s.getIDsForURI(uri) - if len(knownIDs) == 0 { - return nil, err + + // We must clear scopes after loading. + // + // TODO(rfindley): unlike reloadWorkspace, this is simply marking loaded + // packages as loaded. We could do this from snapshot.load and avoid + // raciness. + s.clearShouldLoad(scope) + + // Don't return an error here, as we may still return stale IDs. + // Furthermore, the result of getOrLoadIDsForURI should be consistent upon + // subsequent calls, even if the file is marked as unloadable. + if err != nil && !errors.Is(err, errNoPackages) { + event.Error(ctx, "getOrLoadIDsForURI", err) + } + } + + s.mu.Lock() + ids = s.meta.ids[uri] + if !useInvalidMetadata { + var validIDs []PackageID + for _, id := range ids { + // TODO(rfindley): remove the defensiveness here as well. + if m, ok := s.meta.metadata[id]; ok && m.Valid { + validIDs = append(validIDs, id) + } } + ids = validIDs } - return knownIDs, nil + s.mu.Unlock() + + return ids, nil } // Only use invalid metadata for Go versions >= 1.13. Go 1.12 and below has @@ -617,8 +815,10 @@ func (s *snapshot) GetReverseDependencies(ctx context.Context, id string) ([]sou if err := s.awaitLoaded(ctx); err != nil { return nil, err } - ids := make(map[PackageID]struct{}) - s.transitiveReverseDependencies(PackageID(id), ids) + s.mu.Lock() + meta := s.meta + s.mu.Unlock() + ids := meta.reverseTransitiveClosure(s.useInvalidMetadata(), PackageID(id)) // Make sure to delete the original package ID from the map. delete(ids, PackageID(id)) @@ -639,111 +839,15 @@ func (s *snapshot) checkedPackage(ctx context.Context, id PackageID, mode source if err != nil { return nil, err } - return ph.check(ctx, s) -} - -// transitiveReverseDependencies populates the ids map with package IDs -// belonging to the provided package and its transitive reverse dependencies. -func (s *snapshot) transitiveReverseDependencies(id PackageID, ids map[PackageID]struct{}) { - if _, ok := ids[id]; ok { - return - } - m := s.getMetadata(id) - // Only use invalid metadata if we support it. - if m == nil || !(m.Valid || s.useInvalidMetadata()) { - return - } - ids[id] = struct{}{} - importedBy := s.getImportedBy(id) - for _, parentID := range importedBy { - s.transitiveReverseDependencies(parentID, ids) - } -} - -func (s *snapshot) getGoFile(key parseKey) *parseGoHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.goFiles[key] -} - -func (s *snapshot) addGoFile(key parseKey, pgh *parseGoHandle) *parseGoHandle { - s.mu.Lock() - defer s.mu.Unlock() - if existing, ok := s.goFiles[key]; ok { - return existing - } - s.goFiles[key] = pgh - return pgh -} - -func (s *snapshot) getParseModHandle(uri span.URI) *parseModHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.parseModHandles[uri] -} - -func (s *snapshot) getParseWorkHandle(uri span.URI) *parseWorkHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.parseWorkHandles[uri] -} - -func (s *snapshot) getModWhyHandle(uri span.URI) *modWhyHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.modWhyHandles[uri] -} - -func (s *snapshot) getModTidyHandle(uri span.URI) *modTidyHandle { - s.mu.Lock() - defer s.mu.Unlock() - return s.modTidyHandles[uri] + return ph.await(ctx, s) } func (s *snapshot) getImportedBy(id PackageID) []PackageID { s.mu.Lock() defer s.mu.Unlock() - return s.getImportedByLocked(id) -} - -func (s *snapshot) getImportedByLocked(id PackageID) []PackageID { - // If we haven't rebuilt the import graph since creating the snapshot. - if len(s.meta.importedBy) == 0 { - s.rebuildImportGraph() - } return s.meta.importedBy[id] } -func (s *snapshot) clearAndRebuildImportGraph() { - s.mu.Lock() - defer s.mu.Unlock() - - // Completely invalidate the original map. - s.meta.importedBy = make(map[PackageID][]PackageID) - s.rebuildImportGraph() -} - -func (s *snapshot) rebuildImportGraph() { - for id, m := range s.meta.metadata { - for _, importID := range m.Deps { - s.meta.importedBy[importID] = append(s.meta.importedBy[importID], id) - } - } -} - -func (s *snapshot) addPackageHandle(ph *packageHandle) *packageHandle { - s.mu.Lock() - defer s.mu.Unlock() - - // If the package handle has already been cached, - // return the cached handle instead of overriding it. - if ph, ok := s.packages[ph.packageKey()]; ok { - return ph - } - s.packages[ph.packageKey()] = ph - return ph -} - func (s *snapshot) workspacePackageIDs() (ids []PackageID) { s.mu.Lock() defer s.mu.Unlock() @@ -762,24 +866,20 @@ func (s *snapshot) activePackageIDs() (ids []PackageID) { s.mu.Lock() defer s.mu.Unlock() - seen := make(map[PackageID]bool) for id := range s.workspacePackages { - if s.isActiveLocked(id, seen) { + if s.isActiveLocked(id) { ids = append(ids, id) } } return ids } -func (s *snapshot) isActiveLocked(id PackageID, seen map[PackageID]bool) (active bool) { - if seen == nil { - seen = make(map[PackageID]bool) - } - if seen, ok := seen[id]; ok { +func (s *snapshot) isActiveLocked(id PackageID) (active bool) { + if seen, ok := s.isActivePackageCache.Get(id); ok { return seen } defer func() { - seen[id] = active + s.isActivePackageCache.Set(id, active) }() m, ok := s.meta.metadata[id] if !ok { @@ -790,14 +890,21 @@ func (s *snapshot) isActiveLocked(id PackageID, seen map[PackageID]bool) (active return true } } - for _, dep := range m.Deps { - if s.isActiveLocked(dep, seen) { + // TODO(rfindley): it looks incorrect that we don't also check GoFiles here. + // If a CGo file is open, we want to consider the package active. + for _, dep := range m.DepsByPkgPath { + if s.isActiveLocked(dep) { return true } } return false } +func (s *snapshot) resetIsActivePackageLocked() { + s.isActivePackageCache.Destroy() + s.isActivePackageCache = newIsActivePackageCacheMap() +} + const fileExtensions = "go,mod,sum,work" func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { @@ -811,6 +918,12 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru patterns := map[string]struct{}{ fmt.Sprintf("**/*.{%s}", extensions): {}, } + + if s.view.explicitGowork != "" { + patterns[s.view.explicitGowork.Filename()] = struct{}{} + } + + // Add a pattern for each Go module in the workspace that is not within the view. dirs := s.workspace.dirs(ctx, s) for _, dir := range dirs { dirName := dir.Filename() @@ -830,17 +943,42 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru // contain Go code (golang/go#42348). To handle this, explicitly watch all // of the directories in the workspace. We find them by adding the // directories of every file in the snapshot's workspace directories. - var dirNames []string - for _, uri := range s.getKnownSubdirs(dirs) { - dirNames = append(dirNames, uri.Filename()) - } - sort.Strings(dirNames) - if len(dirNames) > 0 { - patterns[fmt.Sprintf("{%s}", strings.Join(dirNames, ","))] = struct{}{} + // There may be thousands. + if pattern := s.getKnownSubdirsPattern(dirs); pattern != "" { + patterns[pattern] = struct{}{} } + return patterns } +func (s *snapshot) getKnownSubdirsPattern(wsDirs []span.URI) string { + s.mu.Lock() + defer s.mu.Unlock() + + // First, process any pending changes and update the set of known + // subdirectories. + // It may change list of known subdirs and therefore invalidate the cache. + s.applyKnownSubdirsChangesLocked(wsDirs) + + if s.knownSubdirsPatternCache == "" { + var builder strings.Builder + s.knownSubdirs.Range(func(uri span.URI) { + if builder.Len() == 0 { + builder.WriteString("{") + } else { + builder.WriteString(",") + } + builder.WriteString(uri.Filename()) + }) + if builder.Len() > 0 { + builder.WriteString("}") + s.knownSubdirsPatternCache = builder.String() + } + } + + return s.knownSubdirsPatternCache +} + // collectAllKnownSubdirs collects all of the subdirectories within the // snapshot's workspace directories. None of the workspace directories are // included. @@ -850,18 +988,26 @@ func (s *snapshot) collectAllKnownSubdirs(ctx context.Context) { s.mu.Lock() defer s.mu.Unlock() - s.knownSubdirs = map[span.URI]struct{}{} - for uri := range s.files { + s.knownSubdirs.Destroy() + s.knownSubdirs = newKnownDirsSet() + s.knownSubdirsPatternCache = "" + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { s.addKnownSubdirLocked(uri, dirs) - } + }) } -func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) []span.URI { +func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) knownDirsSet { s.mu.Lock() defer s.mu.Unlock() // First, process any pending changes and update the set of known // subdirectories. + s.applyKnownSubdirsChangesLocked(wsDirs) + + return s.knownSubdirs.Clone() +} + +func (s *snapshot) applyKnownSubdirsChangesLocked(wsDirs []span.URI) { for _, c := range s.unprocessedSubdirChanges { if c.isUnchanged { continue @@ -873,19 +1019,13 @@ func (s *snapshot) getKnownSubdirs(wsDirs []span.URI) []span.URI { } } s.unprocessedSubdirChanges = nil - - var result []span.URI - for uri := range s.knownSubdirs { - result = append(result, uri) - } - return result } func (s *snapshot) addKnownSubdirLocked(uri span.URI, dirs []span.URI) { dir := filepath.Dir(uri.Filename()) // First check if the directory is already known, because then we can // return early. - if _, ok := s.knownSubdirs[span.URIFromPath(dir)]; ok { + if s.knownSubdirs.Contains(span.URIFromPath(dir)) { return } var matched span.URI @@ -904,11 +1044,12 @@ func (s *snapshot) addKnownSubdirLocked(uri span.URI, dirs []span.URI) { break } uri := span.URIFromPath(dir) - if _, ok := s.knownSubdirs[uri]; ok { + if s.knownSubdirs.Contains(uri) { break } - s.knownSubdirs[uri] = struct{}{} + s.knownSubdirs.Insert(uri) dir = filepath.Dir(dir) + s.knownSubdirsPatternCache = "" } } @@ -916,11 +1057,12 @@ func (s *snapshot) removeKnownSubdirLocked(uri span.URI) { dir := filepath.Dir(uri.Filename()) for dir != "" { uri := span.URIFromPath(dir) - if _, ok := s.knownSubdirs[uri]; !ok { + if !s.knownSubdirs.Contains(uri) { break } if info, _ := os.Stat(dir); info == nil { - delete(s.knownSubdirs, uri) + s.knownSubdirs.Remove(uri) + s.knownSubdirsPatternCache = "" } dir = filepath.Dir(dir) } @@ -933,29 +1075,14 @@ func (s *snapshot) knownFilesInDir(ctx context.Context, dir span.URI) []span.URI s.mu.Lock() defer s.mu.Unlock() - for uri := range s.files { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { if source.InDir(dir.Filename(), uri.Filename()) { files = append(files, uri) } - } + }) return files } -func (s *snapshot) workspacePackageHandles(ctx context.Context) ([]*packageHandle, error) { - if err := s.awaitLoaded(ctx); err != nil { - return nil, err - } - var phs []*packageHandle - for _, pkgID := range s.workspacePackageIDs() { - ph, err := s.buildPackageHandle(ctx, pkgID, s.workspaceParseMode(pkgID)) - if err != nil { - return nil, err - } - phs = append(phs, ph) - } - return phs, nil -} - func (s *snapshot) ActivePackages(ctx context.Context) ([]source.Package, error) { phs, err := s.activePackageHandles(ctx) if err != nil { @@ -963,7 +1090,7 @@ func (s *snapshot) ActivePackages(ctx context.Context) ([]source.Package, error) } var pkgs []source.Package for _, ph := range phs { - pkg, err := ph.check(ctx, s) + pkg, err := ph.await(ctx, s) if err != nil { return nil, err } @@ -987,28 +1114,40 @@ func (s *snapshot) activePackageHandles(ctx context.Context) ([]*packageHandle, return phs, nil } -func (s *snapshot) Symbols(ctx context.Context) (map[span.URI][]source.Symbol, error) { - result := make(map[span.URI][]source.Symbol) - - // Keep going on errors, but log the first failure. Partial symbol results - // are better than no symbol results. - var firstErr error - for uri, f := range s.files { - sh := s.buildSymbolHandle(ctx, f) - v, err := sh.handle.Get(ctx, s.generation, s) - if err != nil { - if firstErr == nil { - firstErr = err +// Symbols extracts and returns the symbols for each file in all the snapshot's views. +func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol { + var ( + group errgroup.Group + nprocs = 2 * runtime.GOMAXPROCS(-1) // symbolize is a mix of I/O and CPU + iolimit = make(chan struct{}, nprocs) // I/O limiting counting semaphore + resultMu sync.Mutex + result = make(map[span.URI][]source.Symbol) + ) + s.files.Range(func(uri span.URI, f source.VersionedFileHandle) { + if s.View().FileKind(f) != source.Go { + return // workspace symbols currently supports only Go files. + } + + // TODO(adonovan): upgrade errgroup and use group.SetLimit(nprocs). + iolimit <- struct{}{} // acquire token + group.Go(func() error { + defer func() { <-iolimit }() // release token + symbols, err := s.symbolize(ctx, f) + if err != nil { + return err } - continue - } - data := v.(*symbolData) - result[uri] = data.symbols - } - if firstErr != nil { - event.Error(ctx, "getting snapshot symbols", firstErr) + 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 + return result } func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]source.Metadata, error) { @@ -1056,6 +1195,28 @@ func (s *snapshot) KnownPackages(ctx context.Context) ([]source.Package, error) return pkgs, nil } +func (s *snapshot) AllValidMetadata(ctx context.Context) ([]source.Metadata, error) { + if err := s.awaitLoaded(ctx); err != nil { + return nil, err + } + + s.mu.Lock() + defer s.mu.Unlock() + + var meta []source.Metadata + for _, m := range s.meta.metadata { + if m.Valid { + meta = append(meta, m) + } + } + return meta, nil +} + +func (s *snapshot) WorkspacePackageByID(ctx context.Context, id string) (source.Package, error) { + packageID := PackageID(id) + return s.checkedPackage(ctx, packageID, s.workspaceParseMode(packageID)) +} + func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Package, error) { // Don't reload workspace package metadata. // This function is meant to only return currently cached information. @@ -1065,22 +1226,23 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac defer s.mu.Unlock() results := map[string]source.Package{} - for _, ph := range s.packages { - cachedPkg, err := ph.cached(s.generation) + s.packages.Range(func(_, v interface{}) { + cachedPkg, err := v.(*packageHandle).cached() if err != nil { - continue + return } - for importPath, newPkg := range cachedPkg.imports { - if oldPkg, ok := results[string(importPath)]; ok { + for _, newPkg := range cachedPkg.deps { + pkgPath := newPkg.PkgPath() + if oldPkg, ok := results[pkgPath]; ok { // Using the same trick as NarrowestPackage, prefer non-variants. if len(newPkg.compiledGoFiles) < len(oldPkg.(*pkg).compiledGoFiles) { - results[string(importPath)] = newPkg + results[pkgPath] = newPkg } } else { - results[string(importPath)] = newPkg + results[pkgPath] = newPkg } } - } + }) return results, nil } @@ -1101,77 +1263,6 @@ func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { return match } -func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandle { - s.mu.Lock() - defer s.mu.Unlock() - - key := packageKey{ - id: id, - mode: mode, - } - return s.packages[key] -} - -func (s *snapshot) getSymbolHandle(uri span.URI) *symbolHandle { - s.mu.Lock() - defer s.mu.Unlock() - - return s.symbols[uri] -} - -func (s *snapshot) addSymbolHandle(sh *symbolHandle) *symbolHandle { - s.mu.Lock() - defer s.mu.Unlock() - - uri := sh.fh.URI() - // If the package handle has already been cached, - // return the cached handle instead of overriding it. - if sh, ok := s.symbols[uri]; ok { - return sh - } - s.symbols[uri] = sh - return sh -} - -func (s *snapshot) getActionHandle(id PackageID, m source.ParseMode, a *analysis.Analyzer) *actionHandle { - s.mu.Lock() - defer s.mu.Unlock() - - key := actionKey{ - pkg: packageKey{ - id: id, - mode: m, - }, - analyzer: a, - } - return s.actions[key] -} - -func (s *snapshot) addActionHandle(ah *actionHandle) *actionHandle { - s.mu.Lock() - defer s.mu.Unlock() - - key := actionKey{ - analyzer: ah.analyzer, - pkg: packageKey{ - id: ah.pkg.m.ID, - mode: ah.pkg.mode, - }, - } - if ah, ok := s.actions[key]; ok { - return ah - } - s.actions[key] = ah - return ah -} - -func (s *snapshot) getIDsForURI(uri span.URI) []PackageID { - s.mu.Lock() - defer s.mu.Unlock() - - return s.meta.ids[uri] -} - func (s *snapshot) getMetadata(id PackageID) *KnownMetadata { s.mu.Lock() defer s.mu.Unlock() @@ -1179,66 +1270,32 @@ func (s *snapshot) getMetadata(id PackageID) *KnownMetadata { return s.meta.metadata[id] } -func (s *snapshot) shouldLoad(scope interface{}) bool { +// clearShouldLoad clears package IDs that no longer need to be reloaded after +// scopes has been loaded. +func (s *snapshot) clearShouldLoad(scopes ...loadScope) { s.mu.Lock() defer s.mu.Unlock() - switch scope := scope.(type) { - case PackagePath: - var meta *KnownMetadata - for _, m := range s.meta.metadata { - if m.PkgPath != scope { - continue - } - meta = m - } - if meta == nil || meta.ShouldLoad { - return true - } - return false - case fileURI: - uri := span.URI(scope) - ids := s.meta.ids[uri] - if len(ids) == 0 { - return true - } - for _, id := range ids { - m, ok := s.meta.metadata[id] - if !ok || m.ShouldLoad { - return true + for _, scope := range scopes { + switch scope := scope.(type) { + case packageLoadScope: + scopePath := PackagePath(scope) + var toDelete []PackageID + for id, pkgPaths := range s.shouldLoad { + for _, pkgPath := range pkgPaths { + if pkgPath == scopePath { + toDelete = append(toDelete, id) + } + } } - } - return false - default: - return true - } -} - -func (s *snapshot) clearShouldLoad(scope interface{}) { - s.mu.Lock() - defer s.mu.Unlock() - - switch scope := scope.(type) { - case PackagePath: - var meta *KnownMetadata - for _, m := range s.meta.metadata { - if m.PkgPath == scope { - meta = m + for _, id := range toDelete { + delete(s.shouldLoad, id) } - } - if meta == nil { - return - } - meta.ShouldLoad = false - case fileURI: - uri := span.URI(scope) - ids := s.meta.ids[uri] - if len(ids) == 0 { - return - } - for _, id := range ids { - if m, ok := s.meta.metadata[id]; ok { - m.ShouldLoad = false + case fileLoadScope: + uri := span.URI(scope) + ids := s.meta.ids[uri] + for _, id := range ids { + delete(s.shouldLoad, id) } } } @@ -1259,53 +1316,6 @@ func (s *snapshot) noValidMetadataForURILocked(uri span.URI) bool { return true } -// noValidMetadataForID reports whether there is no valid metadata for the -// given ID. -func (s *snapshot) noValidMetadataForID(id PackageID) bool { - s.mu.Lock() - defer s.mu.Unlock() - return s.noValidMetadataForIDLocked(id) -} - -func (s *snapshot) noValidMetadataForIDLocked(id PackageID) bool { - m := s.meta.metadata[id] - return m == nil || !m.Valid -} - -// updateIDForURIsLocked adds the given ID to the set of known IDs for the given URI. -// Any existing invalid IDs are removed from the set of known IDs. IDs that are -// not "command-line-arguments" are preferred, so if a new ID comes in for a -// URI that previously only had "command-line-arguments", the new ID will -// replace the "command-line-arguments" ID. -func (s *snapshot) updateIDForURIsLocked(id PackageID, uris map[span.URI]struct{}) { - for uri := range uris { - // Collect the new set of IDs, preserving any valid existing IDs. - newIDs := []PackageID{id} - for _, existingID := range s.meta.ids[uri] { - // Don't set duplicates of the same ID. - if existingID == id { - continue - } - // If the package previously only had a command-line-arguments ID, - // delete the command-line-arguments workspace package. - if source.IsCommandLineArguments(string(existingID)) { - delete(s.workspacePackages, existingID) - continue - } - // If the metadata for an existing ID is invalid, and we are - // setting metadata for a new, valid ID--don't preserve the old ID. - if m, ok := s.meta.metadata[existingID]; !ok || !m.Valid { - continue - } - newIDs = append(newIDs, existingID) - } - sort.Slice(newIDs, func(i, j int) bool { - return newIDs[i] < newIDs[j] - }) - s.meta.ids[uri] = newIDs - } -} - func (s *snapshot) isWorkspacePackage(id PackageID) bool { s.mu.Lock() defer s.mu.Unlock() @@ -1320,7 +1330,8 @@ func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle { s.mu.Lock() defer s.mu.Unlock() - return s.files[f.URI()] + result, _ := s.files.Get(f.URI()) + return result } // GetVersionedFile returns a File for the given URI. If the file is unknown it @@ -1342,16 +1353,16 @@ func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle } func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.VersionedFileHandle, error) { - if fh, ok := s.files[f.URI()]; ok { + if fh, ok := s.files.Get(f.URI()); ok { return fh, nil } - fh, err := s.view.session.cache.getFile(ctx, f.URI()) + fh, err := s.view.session.cache.getFile(ctx, f.URI()) // read the file if err != nil { return nil, err } closed := &closedFile{fh} - s.files[f.URI()] = closed + s.files.Set(f.URI(), closed) return closed, nil } @@ -1367,16 +1378,21 @@ func (s *snapshot) openFiles() []source.VersionedFileHandle { defer s.mu.Unlock() var open []source.VersionedFileHandle - for _, fh := range s.files { - if s.isOpenLocked(fh.URI()) { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { + if isFileOpen(fh) { open = append(open, fh) } - } + }) return open } func (s *snapshot) isOpenLocked(uri span.URI) bool { - _, open := s.files[uri].(*overlay) + fh, _ := s.files.Get(uri) + return isFileOpen(fh) +} + +func isFileOpen(fh source.VersionedFileHandle) bool { + _, open := fh.(*overlay) return open } @@ -1403,6 +1419,10 @@ func (s *snapshot) awaitLoaded(ctx context.Context) error { } func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { + if wsErr := s.workspace.criticalError(ctx, s); wsErr != nil { + return wsErr + } + loadErr := s.awaitLoadedAllErrors(ctx) if loadErr != nil && errors.Is(loadErr.MainError, context.Canceled) { return nil @@ -1421,6 +1441,8 @@ func (s *snapshot) GetCriticalError(ctx context.Context) *source.CriticalError { // with the user's workspace layout. Workspace packages that only have the // ID "command-line-arguments" are usually a symptom of a bad workspace // configuration. + // + // TODO(rfindley): re-evaluate this heuristic. if containsCommandLineArguments(wsPkgs) { return s.workspaceLayoutError(ctx) } @@ -1462,38 +1484,51 @@ func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) *source.CriticalErr // Do not return results until the snapshot's view has been initialized. s.AwaitInitialized(ctx) - // TODO(rstambler): Should we be more careful about returning the + // TODO(rfindley): Should we be more careful about returning the // initialization error? Is it possible for the initialization error to be // corrected without a successful reinitialization? s.mu.Lock() initializedErr := s.initializedErr s.mu.Unlock() + if initializedErr != nil { return initializedErr } + // TODO(rfindley): revisit this handling. Calling reloadWorkspace with a + // cancelled context should have the same effect, so this preemptive handling + // should not be necessary. + // + // Also: GetCriticalError ignores context cancellation errors. Should we be + // returning nil here? if ctx.Err() != nil { return &source.CriticalError{MainError: ctx.Err()} } + // TODO(rfindley): reloading is not idempotent: if we try to reload or load + // orphaned files below and fail, we won't try again. For that reason, we + // could get different results from subsequent calls to this function, which + // may cause critical errors to be suppressed. + if err := s.reloadWorkspace(ctx); err != nil { diags := s.extractGoCommandErrors(ctx, err) return &source.CriticalError{ - MainError: err, - DiagList: diags, + MainError: err, + Diagnostics: diags, } } + if err := s.reloadOrphanedFiles(ctx); err != nil { diags := s.extractGoCommandErrors(ctx, err) return &source.CriticalError{ - MainError: err, - DiagList: diags, + MainError: err, + Diagnostics: diags, } } return nil } -func (s *snapshot) getInitializationError(ctx context.Context) *source.CriticalError { +func (s *snapshot) getInitializationError() *source.CriticalError { s.mu.Lock() defer s.mu.Unlock() @@ -1513,39 +1548,42 @@ func (s *snapshot) AwaitInitialized(ctx context.Context) { // reloadWorkspace reloads the metadata for all invalidated workspace packages. func (s *snapshot) reloadWorkspace(ctx context.Context) error { - // See which of the workspace packages are missing metadata. + var scopes []loadScope + var seen map[PackagePath]bool s.mu.Lock() - missingMetadata := len(s.workspacePackages) == 0 || len(s.meta.metadata) == 0 - pkgPathSet := map[PackagePath]struct{}{} - for id, pkgPath := range s.workspacePackages { - if m, ok := s.meta.metadata[id]; ok && m.Valid { - continue - } - missingMetadata = true - - // Don't try to reload "command-line-arguments" directly. - if source.IsCommandLineArguments(string(pkgPath)) { - continue + for _, pkgPaths := range s.shouldLoad { + for _, pkgPath := range pkgPaths { + if seen == nil { + seen = make(map[PackagePath]bool) + } + if seen[pkgPath] { + continue + } + seen[pkgPath] = true + scopes = append(scopes, packageLoadScope(pkgPath)) } - pkgPathSet[pkgPath] = struct{}{} } s.mu.Unlock() + if len(scopes) == 0 { + return nil + } + // If the view's build configuration is invalid, we cannot reload by // package path. Just reload the directory instead. - if missingMetadata && !s.ValidBuildConfiguration() { - return s.load(ctx, false, viewLoadScope("LOAD_INVALID_VIEW")) + if !s.ValidBuildConfiguration() { + scopes = []loadScope{viewLoadScope("LOAD_INVALID_VIEW")} } - if len(pkgPathSet) == 0 { - return nil - } + err := s.load(ctx, false, scopes...) - var pkgPaths []interface{} - for pkgPath := range pkgPathSet { - pkgPaths = append(pkgPaths, pkgPath) + // Unless the context was canceled, set "shouldLoad" to false for all + // of the metadata we attempted to load. + if !errors.Is(err, context.Canceled) { + s.clearShouldLoad(scopes...) } - return s.load(ctx, false, pkgPaths...) + + return err } func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { @@ -1556,7 +1594,7 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { files := s.orphanedFiles() // Files without a valid package declaration can't be loaded. Don't try. - var scopes []interface{} + var scopes []loadScope for _, file := range files { pgf, err := s.ParseGo(ctx, file, source.ParseHeader) if err != nil { @@ -1565,7 +1603,8 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { if !pgf.File.Package.IsValid() { continue } - scopes = append(scopes, fileURI(file.URI())) + + scopes = append(scopes, fileLoadScope(file.URI())) } if len(scopes) == 0 { @@ -1589,7 +1628,7 @@ func (s *snapshot) reloadOrphanedFiles(ctx context.Context) error { event.Error(ctx, "reloadOrphanedFiles: failed to load", err, tag.Query.Of(scopes)) s.mu.Lock() for _, scope := range scopes { - uri := span.URI(scope.(fileURI)) + uri := span.URI(scope.(fileLoadScope)) if s.noValidMetadataForURILocked(uri) { s.unloadableFiles[uri] = struct{}{} } @@ -1604,41 +1643,39 @@ func (s *snapshot) orphanedFiles() []source.VersionedFileHandle { defer s.mu.Unlock() var files []source.VersionedFileHandle - for uri, fh := range s.files { + s.files.Range(func(uri span.URI, fh source.VersionedFileHandle) { // Don't try to reload metadata for go.mod files. if s.view.FileKind(fh) != source.Go { - continue + return } // If the URI doesn't belong to this view, then it's not in a workspace // package and should not be reloaded directly. - if !contains(s.view.session.viewsOf(uri), s.view) { - continue + if !source.InDir(s.view.folder.Filename(), uri.Filename()) { + return } // If the file is not open and is in a vendor directory, don't treat it // like a workspace package. if _, ok := fh.(*overlay); !ok && inVendor(uri) { - continue + return } // Don't reload metadata for files we've already deemed unloadable. if _, ok := s.unloadableFiles[uri]; ok { - continue + return } if s.noValidMetadataForURILocked(uri) { files = append(files, fh) } - } + }) return files } -func contains(views []*View, view *View) bool { - for _, v := range views { - if v == view { - return true - } - } - return false -} - +// TODO(golang/go#53756): this function needs to consider more than just the +// absolute URI, for example: +// - the position of /vendor/ with respect to the relevant module root +// - whether or not go.work is in use (as vendoring isn't supported in workspace mode) +// +// Most likely, each call site of inVendor needs to be reconsidered to +// understand and correctly implement the desired behavior. func inVendor(uri span.URI) bool { if !strings.Contains(string(uri), "/vendor/") { return false @@ -1652,33 +1689,6 @@ func inVendor(uri span.URI) bool { return strings.Contains(split[1], "/") } -func generationName(v *View, snapshotID uint64) string { - return fmt.Sprintf("v%v/%v", v.id, snapshotID) -} - -// checkSnapshotLocked verifies that some invariants are preserved on the -// snapshot. -func checkSnapshotLocked(ctx context.Context, s *snapshot) { - // Check that every go file for a workspace package is identified as - // belonging to that workspace package. - for wsID := range s.workspacePackages { - if m, ok := s.meta.metadata[wsID]; ok { - for _, uri := range m.GoFiles { - found := false - for _, id := range s.meta.ids[uri] { - if id == wsID { - found = true - break - } - } - if !found { - log.Error.Logf(ctx, "workspace package %v not associated with %v", wsID, uri) - } - } - } - } -} - // unappliedChanges is a file source that handles an uncloned snapshot. type unappliedChanges struct { originalSnapshot *snapshot @@ -1692,9 +1702,11 @@ func (ac *unappliedChanges) GetFile(ctx context.Context, uri span.URI) (source.F return ac.originalSnapshot.GetFile(ctx, uri) } -func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) *snapshot { - var vendorChanged bool - newWorkspace, workspaceChanged, workspaceReload := s.workspace.invalidate(ctx, changes, &unappliedChanges{ +func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileChange, forceReloadMetadata bool) (*snapshot, func()) { + ctx, done := event.Start(ctx, "snapshot.clone") + defer done() + + newWorkspace, reinit := s.workspace.Clone(ctx, changes, &unappliedChanges{ originalSnapshot: s, changes: changes, }) @@ -1702,97 +1714,87 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC s.mu.Lock() defer s.mu.Unlock() - checkSnapshotLocked(ctx, s) - - newGen := s.view.session.cache.store.Generation(generationName(s.view, s.id+1)) - bgCtx, cancel := context.WithCancel(bgCtx) - result := &snapshot{ - id: s.id + 1, - generation: newGen, - view: s.view, - backgroundCtx: bgCtx, - cancel: cancel, - builtin: s.builtin, - initializeOnce: s.initializeOnce, - initializedErr: s.initializedErr, - meta: NewMetadataGraph(), - packages: make(map[packageKey]*packageHandle, len(s.packages)), - actions: make(map[actionKey]*actionHandle, len(s.actions)), - files: make(map[span.URI]source.VersionedFileHandle, len(s.files)), - goFiles: make(map[parseKey]*parseGoHandle, len(s.goFiles)), - symbols: make(map[span.URI]*symbolHandle, len(s.symbols)), - workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), - unloadableFiles: make(map[span.URI]struct{}, len(s.unloadableFiles)), - parseModHandles: make(map[span.URI]*parseModHandle, len(s.parseModHandles)), - parseWorkHandles: make(map[span.URI]*parseWorkHandle, len(s.parseWorkHandles)), - modTidyHandles: make(map[span.URI]*modTidyHandle, len(s.modTidyHandles)), - modWhyHandles: make(map[span.URI]*modWhyHandle, len(s.modWhyHandles)), - knownSubdirs: make(map[span.URI]struct{}, len(s.knownSubdirs)), - workspace: newWorkspace, - } - - if !workspaceChanged && s.workspaceDirHandle != nil { - result.workspaceDirHandle = s.workspaceDirHandle - newGen.Inherit(s.workspaceDirHandle) - } - - // Copy all of the FileHandles. - for k, v := range s.files { - result.files[k] = v - } - for k, v := range s.symbols { - if change, ok := changes[k]; ok { - if change.exists { - result.symbols[k] = result.buildSymbolHandle(ctx, change.fileHandle) + // If there is an initialization error and a vendor directory changed, try to + // reinit. + if s.initializedErr != nil { + for uri := range changes { + if inVendor(uri) { + reinit = true + break } - continue } - newGen.Inherit(v.handle) - result.symbols[k] = v } + bgCtx, cancel := context.WithCancel(bgCtx) + result := &snapshot{ + id: s.id + 1, + store: s.store, + view: s.view, + backgroundCtx: bgCtx, + cancel: cancel, + builtin: s.builtin, + initialized: s.initialized, + initializedErr: s.initializedErr, + packages: s.packages.Clone(), + isActivePackageCache: s.isActivePackageCache.Clone(), + actions: s.actions.Clone(), + files: s.files.Clone(), + parsedGoFiles: s.parsedGoFiles.Clone(), + parseKeysByURI: s.parseKeysByURI.Clone(), + symbolizeHandles: s.symbolizeHandles.Clone(), + workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), + unloadableFiles: make(map[span.URI]struct{}, len(s.unloadableFiles)), + parseModHandles: s.parseModHandles.Clone(), + parseWorkHandles: s.parseWorkHandles.Clone(), + modTidyHandles: s.modTidyHandles.Clone(), + modWhyHandles: s.modWhyHandles.Clone(), + knownSubdirs: s.knownSubdirs.Clone(), + workspace: newWorkspace, + } + + // The snapshot should be initialized if either s was uninitialized, or we've + // detected a change that triggers reinitialization. + if reinit { + result.initialized = false + } + + // Create a lease on the new snapshot. + // (Best to do this early in case the code below hides an + // incref/decref operation that might destroy it prematurely.) + release := result.Acquire() + // Copy the set of unloadable files. + // + // TODO(rfindley): this looks wrong. Shouldn't we clear unloadableFiles on + // changes to environment or workspace layout, or more generally on any + // metadata change? for k, v := range s.unloadableFiles { result.unloadableFiles[k] = v } - // Copy all of the modHandles. - for k, v := range s.parseModHandles { - result.parseModHandles[k] = v - } - // Copy all of the parseWorkHandles. - for k, v := range s.parseWorkHandles { - result.parseWorkHandles[k] = v - } - for k, v := range s.goFiles { - if _, ok := changes[k.file.URI]; ok { - continue + // TODO(adonovan): merge loops over "changes". + for uri := range changes { + keys, ok := result.parseKeysByURI.Get(uri) + if ok { + for _, key := range keys { + result.parsedGoFiles.Delete(key) + } + result.parseKeysByURI.Delete(uri) } - newGen.Inherit(v.handle) - result.goFiles[k] = v - } - // Copy all of the go.mod-related handles. They may be invalidated later, - // so we inherit them at the end of the function. - for k, v := range s.modTidyHandles { - if _, ok := changes[k]; ok { - continue - } - result.modTidyHandles[k] = v - } - for k, v := range s.modWhyHandles { - if _, ok := changes[k]; ok { - continue - } - result.modWhyHandles[k] = v + // Invalidate go.mod-related handles. + result.modTidyHandles.Delete(uri) + result.modWhyHandles.Delete(uri) + + // Invalidate handles for cached symbols. + result.symbolizeHandles.Delete(uri) } // Add all of the known subdirectories, but don't update them for the // changed files. We need to rebuild the workspace module to know the // true set of known subdirectories, but we don't want to do that in clone. - for k, v := range s.knownSubdirs { - result.knownSubdirs[k] = v - } + result.knownSubdirs = s.knownSubdirs.Clone() + result.knownSubdirsPatternCache = s.knownSubdirsPatternCache for _, c := range changes { result.unprocessedSubdirChanges = append(result.unprocessedSubdirChanges, c) } @@ -1802,23 +1804,25 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC directIDs := map[PackageID]bool{} // Invalidate all package metadata if the workspace module has changed. - if workspaceReload { + if reinit { for k := range s.meta.metadata { directIDs[k] = true } } - changedPkgFiles := map[PackageID]struct{}{} // packages whose file set may have changed - anyImportDeleted := false - for uri, change := range changes { - // Maybe reinitialize the view if we see a change in the vendor - // directory. - if inVendor(uri) { - vendorChanged = true - } + // Compute invalidations based on file changes. + anyImportDeleted := false // import deletions can resolve cycles + anyFileOpenedOrClosed := false // opened files affect workspace packages + anyFileAdded := false // adding a file can resolve missing dependencies + for uri, change := range changes { // The original FileHandle for this URI is cached on the snapshot. - originalFH := s.files[uri] + originalFH, _ := s.files.Get(uri) + var originalOpen, newOpen bool + _, originalOpen = originalFH.(*overlay) + _, newOpen = change.fileHandle.(*overlay) + anyFileOpenedOrClosed = anyFileOpenedOrClosed || (originalOpen != newOpen) + anyFileAdded = anyFileAdded || (originalFH == nil && change.fileHandle != nil) // If uri is a Go file, check if it has changed in a way that would // invalidate metadata. Note that we can't use s.view.FileKind here, @@ -1829,16 +1833,11 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC invalidateMetadata, pkgFileChanged, importDeleted = metadataChanges(ctx, s, originalFH, change.fileHandle) } - invalidateMetadata = invalidateMetadata || forceReloadMetadata || workspaceReload + invalidateMetadata = invalidateMetadata || forceReloadMetadata || reinit anyImportDeleted = anyImportDeleted || importDeleted // Mark all of the package IDs containing the given file. filePackageIDs := invalidatedPackageIDs(uri, s.meta.ids, pkgFileChanged) - if pkgFileChanged { - for id := range filePackageIDs { - changedPkgFiles[id] = struct{}{} - } - } for id := range filePackageIDs { directIDs[id] = directIDs[id] || invalidateMetadata } @@ -1846,22 +1845,21 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC // Invalidate the previous modTidyHandle if any of the files have been // saved or if any of the metadata has been invalidated. if invalidateMetadata || fileWasSaved(originalFH, change.fileHandle) { - // TODO(rstambler): Only delete mod handles for which the - // withoutURI is relevant. - for k := range s.modTidyHandles { - delete(result.modTidyHandles, k) - } - for k := range s.modWhyHandles { - delete(result.modWhyHandles, k) - } + // TODO(maybe): Only delete mod handles for + // which the withoutURI is relevant. + // Requires reverse-engineering the go command. (!) + + result.modTidyHandles.Clear() + result.modWhyHandles.Clear() } - delete(result.parseModHandles, uri) - delete(result.parseWorkHandles, uri) + + result.parseModHandles.Delete(uri) + result.parseWorkHandles.Delete(uri) // Handle the invalidated file; it may have new contents or not exist. if !change.exists { - delete(result.files, uri) + result.files.Delete(uri) } else { - result.files[uri] = change.fileHandle + result.files.Set(uri, change.fileHandle) } // Make sure to remove the changed file from the unloadable set. @@ -1889,8 +1887,23 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } + // Adding a file can resolve missing dependencies from existing packages. + // + // We could be smart here and try to guess which packages may have been + // fixed, but until that proves necessary, just invalidate metadata for any + // package with missing dependencies. + if anyFileAdded { + for id, metadata := range s.meta.metadata { + for _, impID := range metadata.DepsByImpPath { + if impID == "" { // missing import + directIDs[id] = true + break + } + } + } + } + // Invalidate reverse dependencies too. - // TODO(heschi): figure out the locking model and use transitiveReverseDeps? // idsToInvalidate keeps track of transitive reverse dependencies. // If an ID is present in the map, invalidate its types. // If an ID's value is true, invalidate its metadata too. @@ -1906,7 +1919,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC return } idsToInvalidate[id] = newInvalidateMetadata - for _, rid := range s.getImportedByLocked(id) { + for _, rid := range s.meta.importedBy[id] { addRevDeps(rid, invalidateMetadata) } } @@ -1914,28 +1927,14 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC addRevDeps(id, invalidateMetadata) } - // Copy the package type information. - for k, v := range s.packages { - if _, ok := idsToInvalidate[k.id]; ok { - continue - } - newGen.Inherit(v.handle) - result.packages[k] = v - } - // Copy the package analysis information. - for k, v := range s.actions { - if _, ok := idsToInvalidate[k.pkg.id]; ok { - continue - } - newGen.Inherit(v.handle) - result.actions[k] = v - } + result.invalidatePackagesLocked(idsToInvalidate) - // If the workspace mode has changed, we must delete all metadata, as it - // is unusable and may produce confusing or incorrect diagnostics. - // If a file has been deleted, we must delete metadata all packages + // If a file has been deleted, we must delete metadata for all packages // containing that file. - workspaceModeChanged := s.workspaceMode() != result.workspaceMode() + // + // TODO(rfindley): why not keep invalid metadata in this case? If we + // otherwise allow operate on invalid metadata, why not continue to do so, + // skipping the missing file? skipID := map[PackageID]bool{} for _, c := range changes { if c.exists { @@ -1949,122 +1948,86 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC } } - // Collect all of the IDs that are reachable from the workspace packages. - // Any unreachable IDs will have their metadata deleted outright. - reachableID := map[PackageID]bool{} - var addForwardDeps func(PackageID) - addForwardDeps = func(id PackageID) { - if reachableID[id] { - return + // Any packages that need loading in s still need loading in the new + // snapshot. + for k, v := range s.shouldLoad { + if result.shouldLoad == nil { + result.shouldLoad = make(map[PackageID][]PackagePath) } - reachableID[id] = true - m, ok := s.meta.metadata[id] - if !ok { - return - } - for _, depID := range m.Deps { - addForwardDeps(depID) - } - } - for id := range s.workspacePackages { - addForwardDeps(id) + result.shouldLoad[k] = v } - // Copy the URI to package ID mappings, skipping only those URIs whose - // metadata will be reloaded in future calls to load. + // TODO(rfindley): consolidate the this workspace mode detection with + // workspace invalidation. + workspaceModeChanged := s.workspaceMode() != result.workspaceMode() + + // We delete invalid metadata in the following cases: + // - If we are forcing a reload of metadata. + // - If the workspace mode has changed, as stale metadata may produce + // confusing or incorrect diagnostics. + // + // TODO(rfindley): we should probably also clear metadata if we are + // reinitializing the workspace, as otherwise we could leave around a bunch + // of irrelevant and duplicate metadata (for example, if the module path + // changed). However, this breaks the "experimentalUseInvalidMetadata" + // feature, which relies on stale metadata when, for example, a go.mod file + // is broken via invalid syntax. deleteInvalidMetadata := forceReloadMetadata || workspaceModeChanged - idsInSnapshot := map[PackageID]bool{} // track all known IDs - for uri, ids := range s.meta.ids { - var resultIDs []PackageID - for _, id := range ids { - if skipID[id] || deleteInvalidMetadata && idsToInvalidate[id] { - continue - } - // The ID is not reachable from any workspace package, so it should - // be deleted. - if !reachableID[id] { - continue - } - idsInSnapshot[id] = true - resultIDs = append(resultIDs, id) - } - result.meta.ids[uri] = resultIDs - } - // Copy the package metadata. We only need to invalidate packages directly - // containing the affected file, and only if it changed in a relevant way. + // Compute which metadata updates are required. We only need to invalidate + // packages directly containing the affected file, and only if it changed in + // a relevant way. + metadataUpdates := make(map[PackageID]*KnownMetadata) for k, v := range s.meta.metadata { - if !idsInSnapshot[k] { - // Delete metadata for IDs that are no longer reachable from files - // in the snapshot. - continue - } invalidateMetadata := idsToInvalidate[k] - // Mark invalidated metadata rather than deleting it outright. - result.meta.metadata[k] = &KnownMetadata{ - Metadata: v.Metadata, - Valid: v.Valid && !invalidateMetadata, - ShouldLoad: v.ShouldLoad || invalidateMetadata, - } - } - - // Copy the set of initially loaded packages. - for id, pkgPath := range s.workspacePackages { - // Packages with the id "command-line-arguments" are generated by the - // go command when the user is outside of GOPATH and outside of a - // module. Do not cache them as workspace packages for longer than - // necessary. - if source.IsCommandLineArguments(string(id)) { - if invalidateMetadata, ok := idsToInvalidate[id]; invalidateMetadata && ok { - continue - } - } - // If all the files we know about in a package have been deleted, - // the package is gone and we should no longer try to load it. - if m := s.meta.metadata[id]; m != nil { - hasFiles := false - for _, uri := range s.meta.metadata[id].GoFiles { - // For internal tests, we need _test files, not just the normal - // ones. External tests only have _test files, but we can check - // them anyway. - if m.ForTest != "" && !strings.HasSuffix(string(uri), "_test.go") { - continue - } - if _, ok := result.files[uri]; ok { - hasFiles = true - break - } + // For metadata that has been newly invalidated, capture package paths + // requiring reloading in the shouldLoad map. + if invalidateMetadata && !source.IsCommandLineArguments(string(v.ID)) { + if result.shouldLoad == nil { + result.shouldLoad = make(map[PackageID][]PackagePath) } - if !hasFiles { - continue + needsReload := []PackagePath{v.PkgPath} + if v.ForTest != "" && v.ForTest != v.PkgPath { + // When reloading test variants, always reload their ForTest package as + // well. Otherwise, we may miss test variants in the resulting load. + // + // TODO(rfindley): is this actually sufficient? Is it possible that + // other test variants may be invalidated? Either way, we should + // determine exactly what needs to be reloaded here. + needsReload = append(needsReload, v.ForTest) } + result.shouldLoad[k] = needsReload } - // If the package name of a file in the package has changed, it's - // possible that the package ID may no longer exist. Delete it from - // the set of workspace packages, on the assumption that we will add it - // back when the relevant files are reloaded. - if _, ok := changedPkgFiles[id]; ok { + // Check whether the metadata should be deleted. + if skipID[k] || (invalidateMetadata && deleteInvalidMetadata) { + metadataUpdates[k] = nil continue } - result.workspacePackages[id] = pkgPath + // Check if the metadata has changed. + valid := v.Valid && !invalidateMetadata + if valid != v.Valid { + // Mark invalidated metadata rather than deleting it outright. + metadataUpdates[k] = &KnownMetadata{ + Metadata: v.Metadata, + Valid: valid, + } + } } - // Inherit all of the go.mod-related handles. - for _, v := range result.modTidyHandles { - newGen.Inherit(v.handle) - } - for _, v := range result.modWhyHandles { - newGen.Inherit(v.handle) - } - for _, v := range result.parseModHandles { - newGen.Inherit(v.handle) - } - for _, v := range result.parseWorkHandles { - newGen.Inherit(v.handle) + // Update metadata, if necessary. + result.meta = s.meta.Clone(metadataUpdates) + + // Update workspace and active packages, if necessary. + if result.meta != s.meta || anyFileOpenedOrClosed { + result.workspacePackages = computeWorkspacePackagesLocked(result, result.meta) + result.resetIsActivePackageLocked() + } else { + result.workspacePackages = s.workspacePackages } + // Don't bother copying the importedBy graph, // as it changes each time we update metadata. @@ -2073,14 +2036,8 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC if workspaceModeChanged { result.workspacePackages = map[PackageID]PackagePath{} } - - // The snapshot may need to be reinitialized. - if workspaceReload || vendorChanged { - if workspaceChanged || result.initializedErr != nil { - result.initializeOnce = &sync.Once{} - } - } - return result + result.dumpWorkspace("clone") + return result, release } // invalidatedPackageIDs returns all packages invalidated by a change to uri. @@ -2145,6 +2102,35 @@ func invalidatedPackageIDs(uri span.URI, known map[span.URI][]PackageID, package return invalidated } +// invalidatePackagesLocked deletes data associated with the given package IDs. +// +// Note: all keys in the ids map are invalidated, regardless of the +// corresponding value. +// +// s.mu must be held while calling this function. +func (s *snapshot) invalidatePackagesLocked(ids map[PackageID]bool) { + // Delete invalidated package type information. + for id := range ids { + for _, mode := range source.AllParseModes { + key := packageKey{mode, id} + s.packages.Delete(key) + } + } + + // Copy actions. + // TODO(adonovan): opt: avoid iteration over s.actions. + var actionsToDelete []actionKey + s.actions.Range(func(k, _ interface{}) { + key := k.(actionKey) + if _, ok := ids[key.pkgid]; ok { + actionsToDelete = append(actionsToDelete, key) + } + }) + for _, key := range actionsToDelete { + s.actions.Delete(key) + } +} + // fileWasSaved reports whether the FileHandle passed in has been saved. It // accomplishes this by checking to see if the original and current FileHandles // are both overlays, and if the current FileHandle is saved while the original @@ -2250,28 +2236,20 @@ func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH return invalidate, pkgFileChanged, importDeleted } -// peekOrParse returns the cached ParsedGoFile if it exists, otherwise parses -// without caching. +// peekOrParse returns the cached ParsedGoFile if it exists, +// otherwise parses without populating the cache. // // It returns an error if the file could not be read (note that parsing errors // are stored in ParsedGoFile.ParseErr). // // lockedSnapshot must be locked. func peekOrParse(ctx context.Context, lockedSnapshot *snapshot, fh source.FileHandle, mode source.ParseMode) (*source.ParsedGoFile, error) { - key := parseKey{file: fh.FileIdentity(), mode: mode} - if pgh := lockedSnapshot.goFiles[key]; pgh != nil { - cached := pgh.handle.Cached(lockedSnapshot.generation) - if cached != nil { - cached := cached.(*parseGoData) - if cached.parsed != nil { - return cached.parsed, nil - } - } + // Peek in the cache without populating it. + // We do this to reduce retained heap, not work. + if parsed, _ := lockedSnapshot.peekParseGoLocked(fh, mode); parsed != nil { + return parsed, nil // cache hit } - - fset := token.NewFileSet() - data := parseGo(ctx, fset, fh, mode) - return data.parsed, data.err + return parseGoImpl(ctx, token.NewFileSet(), fh, mode) } func magicCommentsChanged(original *ast.File, current *ast.File) bool { @@ -2371,7 +2349,7 @@ func (s *snapshot) BuildGoplsMod(ctx context.Context) (*modfile.File, error) { return buildWorkspaceModFile(ctx, allModules, s) } -// TODO(rfindley): move this to workspacemodule.go +// TODO(rfindley): move this to workspace.go func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, fs source.FileSource) (*modfile.File, error) { file := &modfile.File{} file.AddModuleStmt("gopls-workspace") @@ -2409,8 +2387,8 @@ func buildWorkspaceModFile(ctx context.Context, modFiles map[span.URI]struct{}, goVersion = parsed.Go.Version } path := parsed.Module.Mod.Path - if _, ok := paths[path]; ok { - return nil, fmt.Errorf("found module %q twice in the workspace", path) + if seen, ok := paths[path]; ok { + return nil, fmt.Errorf("found module %q multiple times in the workspace, at:\n\t%q\n\t%q", path, seen, modURI) } paths[path] = modURI // If the module's path includes a major version, we expect it to have diff --git a/gopls/internal/lsp/cache/standalone_go115.go b/gopls/internal/lsp/cache/standalone_go115.go new file mode 100644 index 00000000000..79569ae10ec --- /dev/null +++ b/gopls/internal/lsp/cache/standalone_go115.go @@ -0,0 +1,14 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.16 +// +build !go1.16 + +package cache + +// isStandaloneFile returns false, as the 'standaloneTags' setting is +// unsupported on Go 1.15 and earlier. +func isStandaloneFile(src []byte, standaloneTags []string) bool { + return false +} diff --git a/gopls/internal/lsp/cache/standalone_go116.go b/gopls/internal/lsp/cache/standalone_go116.go new file mode 100644 index 00000000000..2f72d5f5495 --- /dev/null +++ b/gopls/internal/lsp/cache/standalone_go116.go @@ -0,0 +1,50 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.16 +// +build go1.16 + +package cache + +import ( + "go/build/constraint" + "go/parser" + "go/token" +) + +// isStandaloneFile reports whether a file with the given contents should be +// considered a 'standalone main file', meaning a package that consists of only +// a single file. +func isStandaloneFile(src []byte, standaloneTags []string) bool { + f, err := parser.ParseFile(token.NewFileSet(), "", src, parser.PackageClauseOnly|parser.ParseComments) + if err != nil { + return false + } + + if f.Name == nil || f.Name.Name != "main" { + return false + } + + for _, cg := range f.Comments { + // Even with PackageClauseOnly the parser consumes the semicolon following + // the package clause, so we must guard against comments that come after + // the package name. + if cg.Pos() > f.Name.Pos() { + continue + } + for _, comment := range cg.List { + if c, err := constraint.Parse(comment.Text); err == nil { + if tag, ok := c.(*constraint.TagExpr); ok { + for _, t := range standaloneTags { + if t == tag.Tag { + return true + } + } + } + } + } + } + + return false +} diff --git a/gopls/internal/lsp/cache/standalone_go116_test.go b/gopls/internal/lsp/cache/standalone_go116_test.go new file mode 100644 index 00000000000..9adf01e6cea --- /dev/null +++ b/gopls/internal/lsp/cache/standalone_go116_test.go @@ -0,0 +1,96 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.16 +// +build go1.16 + +package cache + +import ( + "testing" +) + +func TestIsStandaloneFile(t *testing.T) { + tests := []struct { + desc string + contents string + standaloneTags []string + want bool + }{ + { + "new syntax", + "//go:build ignore\n\npackage main\n", + []string{"ignore"}, + true, + }, + { + "legacy syntax", + "// +build ignore\n\npackage main\n", + []string{"ignore"}, + true, + }, + { + "multiple tags", + "//go:build ignore\n\npackage main\n", + []string{"exclude", "ignore"}, + true, + }, + { + "invalid tag", + "// +build ignore\n\npackage main\n", + []string{"script"}, + false, + }, + { + "non-main package", + "//go:build ignore\n\npackage p\n", + []string{"ignore"}, + false, + }, + { + "alternate tag", + "// +build script\n\npackage main\n", + []string{"script"}, + true, + }, + { + "both syntax", + "//go:build ignore\n// +build ignore\n\npackage main\n", + []string{"ignore"}, + true, + }, + { + "after comments", + "// A non-directive comment\n//go:build ignore\n\npackage main\n", + []string{"ignore"}, + true, + }, + { + "after package decl", + "package main //go:build ignore\n", + []string{"ignore"}, + false, + }, + { + "on line after package decl", + "package main\n\n//go:build ignore\n", + []string{"ignore"}, + false, + }, + { + "combined with other expressions", + "\n\n//go:build ignore || darwin\n\npackage main\n", + []string{"ignore"}, + false, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + if got := isStandaloneFile([]byte(test.contents), test.standaloneTags); got != test.want { + t.Errorf("isStandaloneFile(%q, %v) = %t, want %t", test.contents, test.standaloneTags, got, test.want) + } + }) + } +} diff --git a/internal/lsp/cache/symbols.go b/gopls/internal/lsp/cache/symbols.go similarity index 73% rename from internal/lsp/cache/symbols.go rename to gopls/internal/lsp/cache/symbols.go index db68912015e..69b2b044273 100644 --- a/internal/lsp/cache/symbols.go +++ b/gopls/internal/lsp/cache/symbols.go @@ -12,52 +12,54 @@ import ( "go/types" "strings" - "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/memoize" ) -type symbolHandle struct { - handle *memoize.Handle +// symbolize returns the result of symbolizing the file identified by fh, using a cache. +func (s *snapshot) symbolize(ctx context.Context, fh source.FileHandle) ([]source.Symbol, error) { + uri := fh.URI() - fh source.FileHandle + s.mu.Lock() + entry, hit := s.symbolizeHandles.Get(uri) + s.mu.Unlock() - // key is the hashed key for the package. - key symbolHandleKey -} + type symbolizeResult struct { + symbols []source.Symbol + err error + } -// symbolData contains the data produced by extracting symbols from a file. -type symbolData struct { - symbols []source.Symbol - err error -} + // Cache miss? + if !hit { + type symbolHandleKey source.Hash + key := symbolHandleKey(fh.FileIdentity().Hash) + promise, release := s.store.Promise(key, func(_ context.Context, arg interface{}) interface{} { + symbols, err := symbolizeImpl(arg.(*snapshot), fh) + return symbolizeResult{symbols, err} + }) -type symbolHandleKey string + entry = promise -func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) *symbolHandle { - if h := s.getSymbolHandle(fh.URI()); h != nil { - return h + s.mu.Lock() + s.symbolizeHandles.Set(uri, entry, func(_, _ interface{}) { release() }) + s.mu.Unlock() } - key := symbolHandleKey(fh.FileIdentity().Hash) - h := s.generation.Bind(key, func(_ context.Context, arg memoize.Arg) interface{} { - snapshot := arg.(*snapshot) - data := &symbolData{} - data.symbols, data.err = symbolize(snapshot, fh) - return data - }, nil) - sh := &symbolHandle{ - handle: h, - fh: fh, - key: key, + // Await result. + v, err := s.awaitPromise(ctx, entry.(*memoize.Promise)) + if err != nil { + return nil, err } - return s.addSymbolHandle(sh) + res := v.(symbolizeResult) + return res.symbols, res.err } -// symbolize extracts symbols from a file. It uses a parsed file already -// present in the cache but otherwise does not populate the cache. -func symbolize(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) { +// symbolizeImpl reads and parses a file and extracts symbols from it. +// It may use a parsed file already present in the cache but +// otherwise does not populate the cache. +func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) { src, err := fh.Read() if err != nil { return nil, err @@ -68,9 +70,13 @@ func symbolize(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error fileDesc *token.File ) - // If the file has already been fully parsed through the cache, we can just - // use the result. - if pgf := snapshot.cachedPGF(fh, source.ParseFull); pgf != nil { + // If the file has already been fully parsed through the + // cache, we can just use the result. But we don't want to + // populate the cache after a miss. + snapshot.mu.Lock() + pgf, _ := snapshot.peekParseGoLocked(fh, source.ParseFull) + snapshot.mu.Unlock() + if pgf != nil { file = pgf.File fileDesc = pgf.Tok } diff --git a/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go similarity index 77% rename from internal/lsp/cache/view.go rename to gopls/internal/lsp/cache/view.go index b0390a3fbde..a408ee7a03a 100644 --- a/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -24,12 +24,14 @@ import ( "golang.org/x/mod/semver" exec "golang.org/x/sys/execabs" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/govulncheck" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -47,10 +49,6 @@ type View struct { // background contexts created for this view. baseCtx context.Context - // cancel is called when all action being performed by the current view - // should be stopped. - cancel context.CancelFunc - // name is the user visible name of this view. name string @@ -59,8 +57,11 @@ type View struct { importsState *importsState - // moduleUpgrades tracks known upgrades for module paths. - moduleUpgrades map[string]string + // moduleUpgrades tracks known upgrades for module paths in each modfile. + // Each modfile has a map of module name to upgrade version. + moduleUpgrades map[span.URI]map[string]string + + vulns map[span.URI][]govulncheck.Vuln // keep track of files by uri and by basename, a single file may be mapped // to multiple uris, and the same basename may map to multiple files @@ -71,8 +72,18 @@ type View struct { // attempt at initialization. initCancelFirstAttempt context.CancelFunc - snapshotMu sync.Mutex - snapshot *snapshot // nil after shutdown has been called + // Track the latest snapshot via the snapshot field, guarded by snapshotMu. + // + // Invariant: whenever the snapshot field is overwritten, destroy(snapshot) + // is called on the previous (overwritten) snapshot while snapshotMu is held, + // incrementing snapshotWG. During shutdown the final snapshot is + // overwritten with nil and destroyed, guaranteeing that all observed + // snapshots have been destroyed via the destroy method, and snapshotWG may + // be waited upon to let these destroy operations complete. + snapshotMu sync.Mutex + snapshot *snapshot // latest snapshot; nil after shutdown has been called + releaseSnapshot func() // called when snapshot is no longer needed + snapshotWG sync.WaitGroup // refcount for pending destroy operations // initialWorkspaceLoad is closed when the first workspace initialization has // completed. If we failed to load, we only retry if the go.mod file changes, @@ -82,12 +93,23 @@ type View struct { // initializationSema is used limit concurrent initialization of snapshots in // the view. We use a channel instead of a mutex to avoid blocking when a // context is canceled. + // + // This field (along with snapshot.initialized) guards against duplicate + // initialization of snapshots. Do not change it without adjusting snapshot + // accordingly. initializationSema chan struct{} // rootURI is the rootURI directory of this view. If we are in GOPATH mode, this // is just the folder. If we are in module mode, this is the module rootURI. rootURI span.URI + // explicitGowork is, if non-empty, the URI for the explicit go.work file + // provided via the users environment. + // + // TODO(rfindley): this is duplicated in the workspace type. Refactor to + // eliminate this duplication. + explicitGowork span.URI + // workspaceInformation tracks various details about this view's // environment variables, go version, and use of modules. workspaceInformation @@ -106,6 +128,9 @@ type workspaceInformation struct { environmentVariables // userGo111Module is the user's value of GO111MODULE. + // + // TODO(rfindley): is there really value in memoizing this variable? It seems + // simpler to make this a function/method. userGo111Module go111module // The value of GO111MODULE we want to run with. @@ -128,6 +153,11 @@ type environmentVariables struct { gocache, gopath, goroot, goprivate, gomodcache, go111module string } +// workspaceMode holds various flags defining how the gopls workspace should +// behave. They may be derived from the environment, user configuration, or +// depend on the Go version. +// +// TODO(rfindley): remove workspace mode, in favor of explicit checks. type workspaceMode int const ( @@ -161,11 +191,12 @@ func (f *fileBase) addURI(uri span.URI) int { func (v *View) ID() string { return v.id } -// tempModFile creates a temporary go.mod file based on the contents of the -// given go.mod file. It is the caller's responsibility to clean up the files -// when they are done using them. +// tempModFile creates a temporary go.mod file based on the contents +// of the given go.mod file. On success, it is the caller's +// responsibility to call the cleanup function when the file is no +// longer needed. func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanup func(), err error) { - filenameHash := hashContents([]byte(modFh.URI().Filename())) + filenameHash := source.Hashf("%s", modFh.URI().Filename()) tmpMod, err := ioutil.TempFile("", fmt.Sprintf("go.%s.*.mod", filenameHash)) if err != nil { return "", nil, err @@ -184,7 +215,9 @@ func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanu return "", nil, err } - cleanup = func() { + // We use a distinct name here to avoid subtlety around the fact + // that both 'return' and 'defer' update the "cleanup" variable. + doCleanup := func() { _ = os.Remove(tmpSumName) _ = os.Remove(tmpURI.Filename()) } @@ -192,7 +225,7 @@ func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanu // Be careful to clean up if we return an error from this function. defer func() { if err != nil { - cleanup() + doCleanup() cleanup = nil } }() @@ -200,11 +233,11 @@ func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanu // Create an analogous go.sum, if one exists. if gosum != nil { if err := ioutil.WriteFile(tmpSumName, gosum, 0655); err != nil { - return "", cleanup, err + return "", nil, err } } - return tmpURI, cleanup, nil + return tmpURI, doCleanup, nil } // Name returns the user visible name of this view. @@ -258,6 +291,9 @@ func minorOptionsChange(a, b *source.Options) bool { if !reflect.DeepEqual(a.DirectoryFilters, b.DirectoryFilters) { return false } + if !reflect.DeepEqual(a.StandaloneTags, b.StandaloneTags) { + return false + } if a.MemoryMode != b.MemoryMode { return false } @@ -284,15 +320,6 @@ func (v *View) SetOptions(ctx context.Context, options *source.Options) (source. return newView, err } -func (v *View) Rebuild(ctx context.Context) (source.Snapshot, func(), error) { - newView, err := v.session.updateView(ctx, v, v.Options()) - if err != nil { - return nil, func() {}, err - } - snapshot, release := newView.Snapshot(ctx) - return snapshot, release, nil -} - func (s *snapshot) WriteEnv(ctx context.Context, w io.Writer) error { s.view.optionsMu.Lock() env := s.view.options.EnvSlice() @@ -371,13 +398,14 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) { relativeTo := s.view.folder.Filename() searched := 0 + filterer := buildFilterer(dir, s.view.gomodcache, s.view.Options()) // Change to WalkDir when we move up to 1.16 err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { if err != nil { return err } relpath := strings.TrimPrefix(path, relativeTo) - excluded := pathExcludedByFilter(relpath, dir, s.view.gomodcache, s.view.options) + excluded := pathExcludedByFilter(relpath, filterer) if fileHasExtension(path, suffixes) && !excluded && !fi.IsDir() { k := span.URIFromPath(path) _, err := s.GetVersionedFile(ctx, k) @@ -397,16 +425,30 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) { } func (v *View) contains(uri span.URI) bool { + // TODO(rfindley): should we ignore the root here? It is not provided by the + // user, and is undefined when go.work is outside the workspace. It would be + // better to explicitly consider the set of active modules wherever relevant. inRoot := source.InDir(v.rootURI.Filename(), uri.Filename()) inFolder := source.InDir(v.folder.Filename(), uri.Filename()) + if !inRoot && !inFolder { return false } - // Filters are applied relative to the workspace folder. - if inFolder { - return !pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), v.rootURI.Filename(), v.gomodcache, v.Options()) + + return !v.filterFunc()(uri) +} + +// filterFunc returns a func that reports whether uri is filtered by the currently configured +// directoryFilters. +func (v *View) filterFunc() func(span.URI) bool { + filterer := buildFilterer(v.rootURI.Filename(), v.gomodcache, v.Options()) + return func(uri span.URI) bool { + // Only filter relative to the configured root directory. + if source.InDirLex(v.folder.Filename(), uri.Filename()) { + return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), v.folder.Filename()), filterer) + } + return false } - return true } func (v *View) mapFile(uri span.URI, f *fileBase) { @@ -433,7 +475,7 @@ func (v *View) relevantChange(c source.FileModification) bool { // TODO(rstambler): Make sure the go.work/gopls.mod files are always known // to the view. for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - if c.URI == uriForSource(v.rootURI, src) { + if c.URI == uriForSource(v.rootURI, v.explicitGowork, src) { return true } } @@ -510,25 +552,26 @@ func (v *View) Shutdown(ctx context.Context) { v.session.removeView(ctx, v) } +// shutdown releases resources associated with the view, and waits for ongoing +// work to complete. +// // TODO(rFindley): probably some of this should also be one in View.Shutdown // above? func (v *View) shutdown(ctx context.Context) { // Cancel the initial workspace load if it is still running. v.initCancelFirstAttempt() - v.mu.Lock() - if v.cancel != nil { - v.cancel() - v.cancel = nil - } - v.mu.Unlock() v.snapshotMu.Lock() if v.snapshot != nil { - go v.snapshot.generation.Destroy("View.shutdown") + v.releaseSnapshot() + v.destroy(v.snapshot, "View.shutdown") v.snapshot = nil + v.releaseSnapshot = nil } v.snapshotMu.Unlock() + v.importsState.destroy() + v.snapshotWG.Wait() } func (v *View) Session() *Session { @@ -538,13 +581,13 @@ func (v *View) Session() *Session { func (s *snapshot) IgnoredFile(uri span.URI) bool { filename := uri.Filename() var prefixes []string - if len(s.workspace.getActiveModFiles()) == 0 { + if len(s.workspace.ActiveModFiles()) == 0 { for _, entry := range filepath.SplitList(s.view.gopath) { prefixes = append(prefixes, filepath.Join(entry, "src")) } } else { prefixes = append(prefixes, s.view.gomodcache) - for m := range s.workspace.getActiveModFiles() { + for m := range s.workspace.ActiveModFiles() { prefixes = append(prefixes, dirURI(m).Filename()) } } @@ -583,7 +626,7 @@ func (v *View) getSnapshot() (*snapshot, func()) { if v.snapshot == nil { panic("getSnapshot called after shutdown") } - return v.snapshot, v.snapshot.generation.Acquire() + return v.snapshot, v.snapshot.Acquire() } func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) { @@ -597,26 +640,36 @@ func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) { <-s.view.initializationSema }() - if s.initializeOnce == nil { + s.mu.Lock() + initialized := s.initialized + s.mu.Unlock() + + if initialized { return } - s.initializeOnce.Do(func() { - s.loadWorkspace(ctx, firstAttempt) - s.collectAllKnownSubdirs(ctx) - }) + + s.loadWorkspace(ctx, firstAttempt) + s.collectAllKnownSubdirs(ctx) } func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { defer func() { - s.initializeOnce = nil + s.mu.Lock() + s.initialized = true + s.mu.Unlock() if firstAttempt { close(s.view.initialWorkspaceLoad) } }() - // If we have multiple modules, we need to load them by paths. - var scopes []interface{} - var modDiagnostics []*source.Diagnostic + // TODO(rFindley): we should only locate template files on the first attempt, + // or guard it via a different mechanism. + s.locateTemplateFiles(ctx) + + // Collect module paths to load by parsing go.mod files. If a module fails to + // parse, capture the parsing failure as a critical diagnostic. + var scopes []loadScope // scopes to load + var modDiagnostics []*source.Diagnostic // diagnostics for broken go.mod files addError := func(uri span.URI, err error) { modDiagnostics = append(modDiagnostics, &source.Diagnostic{ URI: uri, @@ -625,17 +678,23 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { Message: err.Error(), }) } - s.locateTemplateFiles(ctx) - if len(s.workspace.getActiveModFiles()) > 0 { - for modURI := range s.workspace.getActiveModFiles() { + + if len(s.workspace.ActiveModFiles()) > 0 { + for modURI := range s.workspace.ActiveModFiles() { + // Be careful not to add context cancellation errors as critical module + // errors. fh, err := s.GetFile(ctx, modURI) if err != nil { - addError(modURI, err) + if ctx.Err() == nil { + addError(modURI, err) + } continue } parsed, err := s.ParseMod(ctx, fh) if err != nil { - addError(modURI, err) + if ctx.Err() == nil { + addError(modURI, err) + } continue } if parsed.File == nil || parsed.File.Module == nil { @@ -649,12 +708,13 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { scopes = append(scopes, viewLoadScope("LOAD_VIEW")) } - // If we're loading anything, ensure we also load builtin. - // TODO(rstambler): explain the rationale for this. + // If we're loading anything, ensure we also load builtin, + // since it provides fake definitions (and documentation) + // for types like int that are used everywhere. if len(scopes) > 0 { - scopes = append(scopes, PackagePath("builtin")) + scopes = append(scopes, packageLoadScope("builtin")) } - err := s.load(ctx, firstAttempt, scopes...) + err := s.load(ctx, true, scopes...) // If the context is canceled on the first attempt, loading has failed // because the go command has timed out--that should be a critical error. @@ -673,18 +733,18 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) { event.Error(ctx, "initial workspace load failed", err) extractedDiags := s.extractGoCommandErrors(ctx, err) criticalErr = &source.CriticalError{ - MainError: err, - DiagList: append(modDiagnostics, extractedDiags...), + MainError: err, + Diagnostics: append(modDiagnostics, extractedDiags...), } case len(modDiagnostics) == 1: criticalErr = &source.CriticalError{ - MainError: fmt.Errorf(modDiagnostics[0].Message), - DiagList: modDiagnostics, + MainError: fmt.Errorf(modDiagnostics[0].Message), + Diagnostics: modDiagnostics, } case len(modDiagnostics) > 1: criticalErr = &source.CriticalError{ - MainError: fmt.Errorf("error loading module names"), - DiagList: modDiagnostics, + MainError: fmt.Errorf("error loading module names"), + Diagnostics: modDiagnostics, } } @@ -707,23 +767,27 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]*file v.snapshotMu.Lock() defer v.snapshotMu.Unlock() - if v.snapshot == nil { + prevSnapshot, prevReleaseSnapshot := v.snapshot, v.releaseSnapshot + + if prevSnapshot == nil { panic("invalidateContent called after shutdown") } // Cancel all still-running previous requests, since they would be // operating on stale data. - v.snapshot.cancel() + prevSnapshot.cancel() // Do not clone a snapshot until its view has finished initializing. - v.snapshot.AwaitInitialized(ctx) + prevSnapshot.AwaitInitialized(ctx) - oldSnapshot := v.snapshot + // Save one lease of the cloned snapshot in the view. + v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) - v.snapshot = oldSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) - go oldSnapshot.generation.Destroy("View.invalidateContent") + prevReleaseSnapshot() + v.destroy(prevSnapshot, "View.invalidateContent") - return v.snapshot, v.snapshot.generation.Acquire() + // Return a second lease to the caller. + return v.snapshot, v.snapshot.Acquire() } func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, options *source.Options) (*workspaceInformation, error) { @@ -756,9 +820,13 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, } // The value of GOPACKAGESDRIVER is not returned through the go command. gopackagesdriver := os.Getenv("GOPACKAGESDRIVER") + // TODO(rfindley): this looks wrong, or at least overly defensive. If the + // value of GOPACKAGESDRIVER is not returned from the go command... why do we + // look it up here? for _, s := range env { split := strings.SplitN(s, "=", 2) if split[0] == "GOPACKAGESDRIVER" { + bug.Reportf("found GOPACKAGESDRIVER from the go command") // see note above gopackagesdriver = split[1] } } @@ -869,27 +937,6 @@ func defaultCheckPathCase(path string) error { return nil } -func validBuildConfiguration(folder span.URI, ws *workspaceInformation, modFiles map[span.URI]struct{}) bool { - // Since we only really understand the `go` command, if the user has a - // different GOPACKAGESDRIVER, assume that their configuration is valid. - if ws.hasGopackagesDriver { - return true - } - // Check if the user is working within a module or if we have found - // multiple modules in the workspace. - if len(modFiles) > 0 { - return true - } - // The user may have a multiple directories in their GOPATH. - // Check if the workspace is within any of them. - for _, gp := range filepath.SplitList(ws.gopath) { - if source.InDir(filepath.Join(gp, "src"), folder.Filename()) { - return true - } - } - return false -} - // getGoEnv gets the view's various GO* values. func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, go111module string, configEnv []string) (environmentVariables, map[string]string, error) { envVars := environmentVariables{} @@ -907,6 +954,12 @@ func (s *Session) getGoEnv(ctx context.Context, folder string, goversion int, go for k := range vars { args = append(args, k) } + // TODO(rfindley): GOWORK is not a property of the session. It may change + // when a workfile is added or removed. + // + // We need to distinguish between GOWORK values that are set by the GOWORK + // environment variable, and GOWORK values that are computed based on the + // location of a go.work file in the directory hierarchy. args = append(args, "GOWORK") inv := gocommand.Invocation{ @@ -945,26 +998,63 @@ func (v *View) IsGoPrivatePath(target string) bool { return globsMatchPath(v.goprivate, target) } -func (v *View) ModuleUpgrades() map[string]string { +func (v *View) ModuleUpgrades(modfile span.URI) map[string]string { v.mu.Lock() defer v.mu.Unlock() upgrades := map[string]string{} - for mod, ver := range v.moduleUpgrades { + for mod, ver := range v.moduleUpgrades[modfile] { upgrades[mod] = ver } return upgrades } -func (v *View) RegisterModuleUpgrades(upgrades map[string]string) { +func (v *View) RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string) { + // Return early if there are no upgrades. + if len(upgrades) == 0 { + return + } + v.mu.Lock() defer v.mu.Unlock() + m := v.moduleUpgrades[modfile] + if m == nil { + m = make(map[string]string) + v.moduleUpgrades[modfile] = m + } for mod, ver := range upgrades { - v.moduleUpgrades[mod] = ver + m[mod] = ver } } +func (v *View) ClearModuleUpgrades(modfile span.URI) { + v.mu.Lock() + defer v.mu.Unlock() + + delete(v.moduleUpgrades, modfile) +} + +func (v *View) Vulnerabilities(modfile span.URI) []govulncheck.Vuln { + v.mu.Lock() + defer v.mu.Unlock() + + vulns := make([]govulncheck.Vuln, len(v.vulns[modfile])) + copy(vulns, v.vulns[modfile]) + return vulns +} + +func (v *View) SetVulnerabilities(modfile span.URI, vulns []govulncheck.Vuln) { + v.mu.Lock() + defer v.mu.Unlock() + + v.vulns[modfile] = vulns +} + +func (v *View) GoVersion() int { + return v.workspaceInformation.goversion +} + // Copied from // https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/str/path.go;l=58;drc=2910c5b4a01a573ebc97744890a07c1a3122c67a func globsMatchPath(globs, target string) bool { @@ -1040,15 +1130,14 @@ func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modConten return vendorEnabled, nil } -func (v *View) allFilesExcluded(pkg *packages.Package) bool { - opts := v.Options() +func (v *View) allFilesExcluded(pkg *packages.Package, filterer *source.Filterer) bool { folder := filepath.ToSlash(v.folder.Filename()) for _, f := range pkg.GoFiles { f = filepath.ToSlash(f) if !strings.HasPrefix(f, folder) { return false } - if !pathExcludedByFilter(strings.TrimPrefix(f, folder), v.rootURI.Filename(), v.gomodcache, opts) { + if !pathExcludedByFilter(strings.TrimPrefix(f, folder), filterer) { return false } } @@ -1056,8 +1145,9 @@ func (v *View) allFilesExcluded(pkg *packages.Package) bool { } func pathExcludedByFilterFunc(root, gomodcache string, opts *source.Options) func(string) bool { + filterer := buildFilterer(root, gomodcache, opts) return func(path string) bool { - return pathExcludedByFilter(path, root, gomodcache, opts) + return pathExcludedByFilter(path, filterer) } } @@ -1067,12 +1157,18 @@ func pathExcludedByFilterFunc(root, gomodcache string, opts *source.Options) fun // TODO(rfindley): passing root and gomodcache here makes it confusing whether // path should be absolute or relative, and has already caused at least one // bug. -func pathExcludedByFilter(path, root, gomodcache string, opts *source.Options) bool { +func pathExcludedByFilter(path string, filterer *source.Filterer) bool { path = strings.TrimPrefix(filepath.ToSlash(path), "/") + return filterer.Disallow(path) +} + +func buildFilterer(root, gomodcache string, opts *source.Options) *source.Filterer { + // TODO(rfindley): this looks wrong. If gomodcache isn't actually nested + // under root, this will do the wrong thing. gomodcache = strings.TrimPrefix(filepath.ToSlash(strings.TrimPrefix(gomodcache, root)), "/") filters := opts.DirectoryFilters if gomodcache != "" { filters = append(filters, "-"+gomodcache) } - return source.FiltersDisallow(path, filters) + return source.NewFilterer(filters) } diff --git a/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go similarity index 93% rename from internal/lsp/cache/view_test.go rename to gopls/internal/lsp/cache/view_test.go index d76dcda8ed4..99daff13b01 100644 --- a/internal/lsp/cache/view_test.go +++ b/gopls/internal/lsp/cache/view_test.go @@ -10,9 +10,9 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) func TestCaseInsensitiveFilesystem(t *testing.T) { @@ -161,15 +161,14 @@ func TestFilters(t *testing.T) { } for _, tt := range tests { - opts := &source.Options{} - opts.DirectoryFilters = tt.filters + filterer := source.NewFilterer(tt.filters) for _, inc := range tt.included { - if pathExcludedByFilter(inc, "root", "root/gopath/pkg/mod", opts) { + if pathExcludedByFilter(inc, filterer) { t.Errorf("filters %q excluded %v, wanted included", tt.filters, inc) } } for _, exc := range tt.excluded { - if !pathExcludedByFilter(exc, "root", "root/gopath/pkg/mod", opts) { + if !pathExcludedByFilter(exc, filterer) { t.Errorf("filters %q included %v, wanted excluded", tt.filters, exc) } } diff --git a/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go similarity index 79% rename from internal/lsp/cache/workspace.go rename to gopls/internal/lsp/cache/workspace.go index 669ce9290c9..b280ef369a8 100644 --- a/internal/lsp/cache/workspace.go +++ b/gopls/internal/lsp/cache/workspace.go @@ -15,9 +15,9 @@ import ( "sync" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -28,7 +28,7 @@ const ( legacyWorkspace = iota // non-module or single module mode goplsModWorkspace // modules provided by a gopls.mod file goWorkWorkspace // modules provided by a go.work file - fileSystemWorkspace // modules scanned from the filesystem + fileSystemWorkspace // modules found by walking the filesystem ) func (s workspaceSource) String() string { @@ -46,6 +46,19 @@ func (s workspaceSource) String() string { } } +// workspaceCommon holds immutable information about the workspace setup. +// +// TODO(rfindley): there is some redundancy here with workspaceInformation. +// Reconcile these two types. +type workspaceCommon struct { + root span.URI + excludePath func(string) bool + + // explicitGowork is, if non-empty, the URI for the explicit go.work file + // provided via the user's environment. + explicitGowork span.URI +} + // workspace tracks go.mod files in the workspace, along with the // gopls.mod file, to provide support for multi-module workspaces. // @@ -58,8 +71,9 @@ func (s workspaceSource) String() string { // This type is immutable (or rather, idempotent), so that it may be shared // across multiple snapshots. type workspace struct { - root span.URI - excludePath func(string) bool + workspaceCommon + + // The source of modules in this workspace. moduleSource workspaceSource // activeModFiles holds the active go.mod files. @@ -95,15 +109,24 @@ type workspace struct { // // If there is no active workspace file (a gopls.mod or go.work), newWorkspace // scans the filesystem to find modules. -func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff bool, useWsModule bool) (*workspace, error) { +// +// TODO(rfindley): newWorkspace should perhaps never fail, relying instead on +// the criticalError method to surface problems in the workspace. +func newWorkspace(ctx context.Context, root, explicitGowork span.URI, fs source.FileSource, excludePath func(string) bool, go111moduleOff, useWsModule bool) (*workspace, error) { ws := &workspace{ - root: root, - excludePath: excludePath, + workspaceCommon: workspaceCommon{ + root: root, + explicitGowork: explicitGowork, + excludePath: excludePath, + }, } // The user may have a gopls.mod or go.work file that defines their // workspace. - if err := loadExplicitWorkspaceFile(ctx, ws, fs); err == nil { + // + // TODO(rfindley): if GO111MODULE=off, this looks wrong, though there are + // probably other problems. + if err := ws.loadExplicitWorkspaceFile(ctx, fs); err == nil { return ws, nil } @@ -135,15 +158,15 @@ func newWorkspace(ctx context.Context, root span.URI, fs source.FileSource, excl // loadExplicitWorkspaceFile loads workspace information from go.work or // gopls.mod files, setting the active modules, mod file, and module source // accordingly. -func loadExplicitWorkspaceFile(ctx context.Context, ws *workspace, fs source.FileSource) error { +func (ws *workspace) loadExplicitWorkspaceFile(ctx context.Context, fs source.FileSource) error { for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - fh, err := fs.GetFile(ctx, uriForSource(ws.root, src)) + fh, err := fs.GetFile(ctx, uriForSource(ws.root, ws.explicitGowork, src)) if err != nil { return err } contents, err := fh.Read() if err != nil { - continue + continue // TODO(rfindley): is it correct to proceed here? } var file *modfile.File var activeModFiles map[span.URI]struct{} @@ -170,14 +193,38 @@ func loadExplicitWorkspaceFile(ctx context.Context, ws *workspace, fs source.Fil var noHardcodedWorkspace = errors.New("no hardcoded workspace") +// TODO(rfindley): eliminate getKnownModFiles. func (w *workspace) getKnownModFiles() map[span.URI]struct{} { return w.knownModFiles } -func (w *workspace) getActiveModFiles() map[span.URI]struct{} { +// ActiveModFiles returns the set of active mod files for the current workspace. +func (w *workspace) ActiveModFiles() map[span.URI]struct{} { return w.activeModFiles } +// criticalError returns a critical error related to the workspace setup. +func (w *workspace) criticalError(ctx context.Context, fs source.FileSource) (res *source.CriticalError) { + // For now, we narrowly report errors related to `go.work` files. + // + // TODO(rfindley): investigate whether other workspace validation errors + // can be consolidated here. + if w.moduleSource == goWorkWorkspace { + // We should have already built the modfile, but build here to be + // consistent about accessing w.mod after w.build. + // + // TODO(rfindley): build eagerly. Building lazily is a premature + // optimization that poses a significant burden on the code. + w.build(ctx, fs) + if w.buildErr != nil { + return &source.CriticalError{ + MainError: w.buildErr, + } + } + } + return nil +} + // modFile gets the workspace modfile associated with this workspace, // computing it if it doesn't exist. // @@ -207,9 +254,10 @@ func (w *workspace) build(ctx context.Context, fs source.FileSource) { // would not be obvious to the user how to recover. ctx = xcontext.Detach(ctx) - // If our module source is not gopls.mod, try to build the workspace module - // from modules. Fall back on the pre-existing mod file if parsing fails. - if w.moduleSource != goplsModWorkspace { + // If the module source is from the filesystem, try to build the workspace + // module from active modules discovered by scanning the filesystem. Fall + // back on the pre-existing mod file if parsing fails. + if w.moduleSource == fileSystemWorkspace { file, err := buildWorkspaceModFile(ctx, w.activeModFiles, fs) switch { case err == nil: @@ -222,6 +270,7 @@ func (w *workspace) build(ctx context.Context, fs source.FileSource) { w.buildErr = err } } + if w.mod != nil { w.wsDirs = map[span.URI]struct{}{ w.root: {}, @@ -235,18 +284,21 @@ func (w *workspace) build(ctx context.Context, fs source.FileSource) { w.wsDirs[span.URIFromPath(r.New.Path)] = struct{}{} } } + // Ensure that there is always at least the root dir. if len(w.wsDirs) == 0 { w.wsDirs = map[span.URI]struct{}{ w.root: {}, } } + sum, err := buildWorkspaceSumFile(ctx, w.activeModFiles, fs) if err == nil { w.sum = sum } else { event.Error(ctx, "building workspace sum file", err) } + w.built = true } @@ -263,30 +315,32 @@ func (w *workspace) dirs(ctx context.Context, fs source.FileSource) []span.URI { return dirs } -// invalidate returns a (possibly) new workspace after invalidating the changed +// Clone returns a (possibly) new workspace after invalidating the changed // files. If w is still valid in the presence of changedURIs, it returns itself // unmodified. // -// The returned changed and reload flags control the level of invalidation. -// Some workspace changes may affect workspace contents without requiring a -// reload of metadata (for example, unsaved changes to a go.mod or go.sum -// file). -func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, changed, reload bool) { +// The returned needReinit flag indicates to the caller that the workspace +// needs to be reinitialized (because a relevant go.mod or go.work file has +// been changed). +// +// TODO(rfindley): it looks wrong that we return 'needReinit' here. The caller +// should determine whether to re-initialize.. +func (w *workspace) Clone(ctx context.Context, changes map[span.URI]*fileChange, fs source.FileSource) (_ *workspace, needReinit bool) { // Prevent races to w.modFile or w.wsDirs below, if w has not yet been built. w.buildMu.Lock() defer w.buildMu.Unlock() // Clone the workspace. This may be discarded if nothing changed. + changed := false result := &workspace{ - root: w.root, - moduleSource: w.moduleSource, - knownModFiles: make(map[span.URI]struct{}), - activeModFiles: make(map[span.URI]struct{}), - workFile: w.workFile, - mod: w.mod, - sum: w.sum, - wsDirs: w.wsDirs, - excludePath: w.excludePath, + workspaceCommon: w.workspaceCommon, + moduleSource: w.moduleSource, + knownModFiles: make(map[span.URI]struct{}), + activeModFiles: make(map[span.URI]struct{}), + workFile: w.workFile, + mod: w.mod, + sum: w.sum, + wsDirs: w.wsDirs, } for k, v := range w.knownModFiles { result.knownModFiles[k] = v @@ -300,7 +354,7 @@ func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileCh // determine which modules we care about. If go.work/gopls.mod has changed // we need to either re-read it if it exists or walk the filesystem if it // has been deleted. go.work should override the gopls.mod if both exist. - changed, reload = handleWorkspaceFileChanges(ctx, result, changes, fs) + changed, needReinit = handleWorkspaceFileChanges(ctx, result, changes, fs) // Next, handle go.mod changes that could affect our workspace. for uri, change := range changes { // Otherwise, we only care about go.mod files in the workspace directory. @@ -309,7 +363,7 @@ func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileCh } changed = true active := result.moduleSource != legacyWorkspace || source.CompareURI(modURI(w.root), uri) == 0 - reload = reload || (active && change.fileHandle.Saved()) + needReinit = needReinit || (active && change.fileHandle.Saved()) // Don't mess with the list of mod files if using go.work or gopls.mod. if result.moduleSource == goplsModWorkspace || result.moduleSource == goWorkWorkspace { continue @@ -339,14 +393,14 @@ func (w *workspace) invalidate(ctx context.Context, changes map[span.URI]*fileCh // Only changes to active go.sum files actually cause the workspace to // change. changed = true - reload = reload || change.fileHandle.Saved() + needReinit = needReinit || change.fileHandle.Saved() } if !changed { - return w, false, false + return w, false } - return result, changed, reload + return result, needReinit } // handleWorkspaceFileChanges handles changes related to a go.work or gopls.mod @@ -356,7 +410,7 @@ func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[ // exists or walk the filesystem if it has been deleted. // go.work should override the gopls.mod if both exist. for _, src := range []workspaceSource{goWorkWorkspace, goplsModWorkspace} { - uri := uriForSource(ws.root, src) + uri := uriForSource(ws.root, ws.explicitGowork, src) // File opens/closes are just no-ops. change, ok := changes[uri] if !ok { @@ -381,6 +435,11 @@ func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[ // An unparseable file should not invalidate the workspace: // nothing good could come from changing the workspace in // this case. + // + // TODO(rfindley): well actually, it could potentially lead to a better + // critical error. Evaluate whether we can unify this case with the + // error returned by newWorkspace, without needlessly invalidating + // metadata. event.Error(ctx, fmt.Sprintf("parsing %s", filepath.Base(uri.Filename())), err) } else { // only update the modfile if it parsed. @@ -420,12 +479,15 @@ func handleWorkspaceFileChanges(ctx context.Context, ws *workspace, changes map[ } // goplsModURI returns the URI for the gopls.mod file contained in root. -func uriForSource(root span.URI, src workspaceSource) span.URI { +func uriForSource(root, explicitGowork span.URI, src workspaceSource) span.URI { var basename string switch src { case goplsModWorkspace: basename = "gopls.mod" case goWorkWorkspace: + if explicitGowork != "" { + return explicitGowork + } basename = "go.work" default: return "" @@ -501,6 +563,10 @@ func parseGoWork(ctx context.Context, root, uri span.URI, contents []byte, fs so modURI := span.URIFromPath(filepath.Join(dir.Path, "go.mod")) modFiles[modURI] = struct{}{} } + + // TODO(rfindley): we should either not build the workspace modfile here, or + // not fail so hard. A failure in building the workspace modfile should not + // invalidate the active module paths extracted above. modFile, err := buildWorkspaceModFile(ctx, modFiles, fs) if err != nil { return nil, nil, err diff --git a/internal/lsp/cache/workspace_test.go b/gopls/internal/lsp/cache/workspace_test.go similarity index 94% rename from internal/lsp/cache/workspace_test.go rename to gopls/internal/lsp/cache/workspace_test.go index b809ad196a6..188869562c5 100644 --- a/internal/lsp/cache/workspace_test.go +++ b/gopls/internal/lsp/cache/workspace_test.go @@ -12,9 +12,9 @@ import ( "testing" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) // osFileSource is a fileSource that just reads from the operating system. @@ -280,7 +280,7 @@ replace gopls.test => ../../gopls.test2`, false}, fs := &osFileSource{} excludeNothing := func(string) bool { return false } - w, err := newWorkspace(ctx, root, fs, excludeNothing, false, !test.legacyMode) + w, err := newWorkspace(ctx, root, "", fs, excludeNothing, false, !test.legacyMode) if err != nil { t.Fatal(err) } @@ -298,12 +298,13 @@ replace gopls.test => ../../gopls.test2`, false}, t.Fatal(err) } } - got, gotChanged, gotReload := w.invalidate(ctx, changes, fs) + got, gotReinit := w.Clone(ctx, changes, fs) + gotChanged := got != w if gotChanged != test.wantChanged { t.Errorf("w.invalidate(): got changed %t, want %t", gotChanged, test.wantChanged) } - if gotReload != test.wantReload { - t.Errorf("w.invalidate(): got reload %t, want %t", gotReload, test.wantReload) + if gotReinit != test.wantReload { + t.Errorf("w.invalidate(): got reload %t, want %t", gotReinit, test.wantReload) } checkState(ctx, t, fs, rel, got, test.finalState) } @@ -324,7 +325,7 @@ func workspaceFromTxtar(t *testing.T, files string) (*workspace, func(), error) fs := &osFileSource{} excludeNothing := func(string) bool { return false } - workspace, err := newWorkspace(ctx, root, fs, excludeNothing, false, false) + workspace, err := newWorkspace(ctx, root, "", fs, excludeNothing, false, false) return workspace, cleanup, err } @@ -385,7 +386,7 @@ func checkState(ctx context.Context, t *testing.T, fs source.FileSource, rel fak t.Errorf("module source = %v, want %v", got.moduleSource, want.source) } modules := make(map[span.URI]struct{}) - for k := range got.getActiveModFiles() { + for k := range got.ActiveModFiles() { modules[k] = struct{}{} } for _, modPath := range want.modules { diff --git a/internal/lsp/call_hierarchy.go b/gopls/internal/lsp/call_hierarchy.go similarity index 92% rename from internal/lsp/call_hierarchy.go rename to gopls/internal/lsp/call_hierarchy.go index 43c4ea8d5b7..79eeb25cc15 100644 --- a/internal/lsp/call_hierarchy.go +++ b/gopls/internal/lsp/call_hierarchy.go @@ -7,8 +7,8 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) prepareCallHierarchy(ctx context.Context, params *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) { diff --git a/internal/lsp/cmd/call_hierarchy.go b/gopls/internal/lsp/cmd/call_hierarchy.go similarity index 94% rename from internal/lsp/cmd/call_hierarchy.go rename to gopls/internal/lsp/cmd/call_hierarchy.go index c9f9e73e0e2..295dea8b0d4 100644 --- a/internal/lsp/cmd/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/call_hierarchy.go @@ -10,8 +10,8 @@ import ( "fmt" "strings" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -47,7 +47,7 @@ func (c *callHierarchy) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -114,7 +114,7 @@ func (c *callHierarchy) Run(ctx context.Context, args ...string) error { // callItemPrintString returns a protocol.CallHierarchyItem object represented as a string. // item and call ranges (protocol.Range) are converted to user friendly spans (1-indexed). func callItemPrintString(ctx context.Context, conn *connection, item protocol.CallHierarchyItem, callsURI protocol.DocumentURI, calls []protocol.Range) (string, error) { - itemFile := conn.AddFile(ctx, item.URI.SpanURI()) + itemFile := conn.openFile(ctx, item.URI.SpanURI()) if itemFile.err != nil { return "", itemFile.err } @@ -123,7 +123,7 @@ func callItemPrintString(ctx context.Context, conn *connection, item protocol.Ca return "", err } - callsFile := conn.AddFile(ctx, callsURI.SpanURI()) + callsFile := conn.openFile(ctx, callsURI.SpanURI()) if callsURI != "" && callsFile.err != nil { return "", callsFile.err } diff --git a/internal/lsp/cmd/capabilities_test.go b/gopls/internal/lsp/cmd/capabilities_test.go similarity index 95% rename from internal/lsp/cmd/capabilities_test.go rename to gopls/internal/lsp/cmd/capabilities_test.go index 1d01b4bd0d7..bd209639420 100644 --- a/internal/lsp/cmd/capabilities_test.go +++ b/gopls/internal/lsp/cmd/capabilities_test.go @@ -12,9 +12,9 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // TestCapabilities does some minimal validation of the server's adherence to the LSP. @@ -43,7 +43,7 @@ func TestCapabilities(t *testing.T) { params.Capabilities.Workspace.Configuration = true // Send an initialize request to the server. - c.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), c.Client) + c.Server = lsp.NewServer(cache.New(nil, nil, app.options).NewSession(ctx), c.Client) result, err := c.Server.Initialize(ctx, params) if err != nil { t.Fatal(err) diff --git a/internal/lsp/cmd/check.go b/gopls/internal/lsp/cmd/check.go similarity index 95% rename from internal/lsp/cmd/check.go rename to gopls/internal/lsp/cmd/check.go index 9a136699270..cf081ca2615 100644 --- a/internal/lsp/cmd/check.go +++ b/gopls/internal/lsp/cmd/check.go @@ -9,7 +9,7 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // check implements the check verb for gopls. @@ -48,7 +48,7 @@ func (c *check) Run(ctx context.Context, args ...string) error { for _, arg := range args { uri := span.URIFromPath(arg) uris = append(uris, uri) - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } diff --git a/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go similarity index 94% rename from internal/lsp/cmd/cmd.go rename to gopls/internal/lsp/cmd/cmd.go index a81eb839535..5c64e108668 100644 --- a/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -22,14 +22,14 @@ import ( "text/tabwriter" "time" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" "golang.org/x/tools/internal/xcontext" ) @@ -286,7 +286,7 @@ func (app *Application) connect(ctx context.Context) (*connection, error) { switch { case app.Remote == "": connection := newConnection(app) - connection.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), connection.Client) + connection.Server = lsp.NewServer(cache.New(nil, nil, app.options).NewSession(ctx), connection.Client) ctx = protocol.WithClient(ctx, connection.Client) return connection, connection.initialize(ctx, app.options) case strings.HasPrefix(app.Remote, "internal@"): @@ -399,7 +399,7 @@ type cmdFile struct { uri span.URI mapper *protocol.ColumnMapper err error - added bool + open bool diagnostics []protocol.Diagnostic } @@ -422,6 +422,10 @@ func fileURI(uri protocol.DocumentURI) span.URI { return sURI } +func (c *cmdClient) CodeLensRefresh(context.Context) error { return nil } + +func (c *cmdClient) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } + func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil } func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { @@ -554,22 +558,24 @@ func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { return file } -func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile { - c.Client.filesMu.Lock() - defer c.Client.filesMu.Unlock() +func (c *cmdClient) openFile(ctx context.Context, uri span.URI) *cmdFile { + c.filesMu.Lock() + defer c.filesMu.Unlock() - file := c.Client.getFile(ctx, uri) - // This should never happen. - if file == nil { - return &cmdFile{ - uri: uri, - err: fmt.Errorf("no file found for %s", uri), - } + file := c.getFile(ctx, uri) + if file.err != nil || file.open { + return file } - if file.err != nil || file.added { + file.open = true + return file +} + +func (c *connection) openFile(ctx context.Context, uri span.URI) *cmdFile { + file := c.Client.openFile(ctx, uri) + if file.err != nil { return file } - file.added = true + p := &protocol.DidOpenTextDocumentParams{ TextDocument: protocol.TextDocumentItem{ URI: protocol.URIFromSpanURI(uri), diff --git a/internal/lsp/cmd/definition.go b/gopls/internal/lsp/cmd/definition.go similarity index 94% rename from internal/lsp/cmd/definition.go rename to gopls/internal/lsp/cmd/definition.go index 44e6fc8c717..edfd7392902 100644 --- a/internal/lsp/cmd/definition.go +++ b/gopls/internal/lsp/cmd/definition.go @@ -12,9 +12,9 @@ import ( "os" "strings" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -80,7 +80,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { } defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -113,7 +113,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error { if hover == nil { return fmt.Errorf("%v: not an identifier", from) } - file = conn.AddFile(ctx, fileURI(locs[0].URI)) + file = conn.openFile(ctx, fileURI(locs[0].URI)) if file.err != nil { return fmt.Errorf("%v: %v", from, file.err) } diff --git a/internal/lsp/cmd/folding_range.go b/gopls/internal/lsp/cmd/folding_range.go similarity index 92% rename from internal/lsp/cmd/folding_range.go rename to gopls/internal/lsp/cmd/folding_range.go index 513c9bdd227..7a9cbf9e8fb 100644 --- a/internal/lsp/cmd/folding_range.go +++ b/gopls/internal/lsp/cmd/folding_range.go @@ -9,8 +9,8 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -44,7 +44,7 @@ func (r *foldingRanges) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } diff --git a/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go similarity index 84% rename from internal/lsp/cmd/format.go rename to gopls/internal/lsp/cmd/format.go index 5e17ed4a570..2b8109c670a 100644 --- a/internal/lsp/cmd/format.go +++ b/gopls/internal/lsp/cmd/format.go @@ -10,10 +10,10 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/diff" ) // format implements the format verb for gopls. @@ -57,7 +57,7 @@ func (c *format) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) for _, arg := range args { spn := span.Parse(arg) - file := conn.AddFile(ctx, spn.URI()) + file := conn.openFile(ctx, spn.URI()) if file.err != nil { return file.err } @@ -76,11 +76,10 @@ func (c *format) Run(ctx context.Context, args ...string) error { if err != nil { return fmt.Errorf("%v: %v", spn, err) } - sedits, err := source.FromProtocolEdits(file.mapper, edits) + formatted, sedits, err := source.ApplyProtocolEdits(file.mapper, edits) if err != nil { return fmt.Errorf("%v: %v", spn, err) } - formatted := diff.ApplyEdits(string(file.mapper.Content), sedits) printIt := true if c.List { printIt = false @@ -96,8 +95,11 @@ func (c *format) Run(ctx context.Context, args ...string) error { } if c.Diff { printIt = false - u := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) - fmt.Print(u) + unified, err := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + if err != nil { + return err + } + fmt.Print(unified) } if printIt { fmt.Print(formatted) diff --git a/internal/lsp/cmd/help_test.go b/gopls/internal/lsp/cmd/help_test.go similarity index 97% rename from internal/lsp/cmd/help_test.go rename to gopls/internal/lsp/cmd/help_test.go index 536d19dc219..f8d9b0b75ca 100644 --- a/internal/lsp/cmd/help_test.go +++ b/gopls/internal/lsp/cmd/help_test.go @@ -12,7 +12,7 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/cmd" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go similarity index 93% rename from internal/lsp/cmd/highlight.go rename to gopls/internal/lsp/cmd/highlight.go index a325a2d53d9..0737e9c424a 100644 --- a/internal/lsp/cmd/highlight.go +++ b/gopls/internal/lsp/cmd/highlight.go @@ -10,8 +10,8 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -47,7 +47,7 @@ func (r *highlight) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } diff --git a/internal/lsp/cmd/implementation.go b/gopls/internal/lsp/cmd/implementation.go similarity index 91% rename from internal/lsp/cmd/implementation.go rename to gopls/internal/lsp/cmd/implementation.go index 7b42d994303..dbc5fc3223b 100644 --- a/internal/lsp/cmd/implementation.go +++ b/gopls/internal/lsp/cmd/implementation.go @@ -10,8 +10,8 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -47,7 +47,7 @@ func (i *implementation) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -71,7 +71,7 @@ func (i *implementation) Run(ctx context.Context, args ...string) error { var spans []string for _, impl := range implementations { - f := conn.AddFile(ctx, fileURI(impl.URI)) + f := conn.openFile(ctx, fileURI(impl.URI)) span, err := f.mapper.Span(impl) if err != nil { return err diff --git a/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go similarity index 79% rename from internal/lsp/cmd/imports.go rename to gopls/internal/lsp/cmd/imports.go index 49778603d23..fadc8466834 100644 --- a/internal/lsp/cmd/imports.go +++ b/gopls/internal/lsp/cmd/imports.go @@ -10,10 +10,10 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/tool" ) @@ -56,7 +56,7 @@ func (t *imports) Run(ctx context.Context, args ...string) error { from := span.Parse(args[0]) uri := from.URI() - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } @@ -74,17 +74,17 @@ func (t *imports) Run(ctx context.Context, args ...string) error { continue } for _, c := range a.Edit.DocumentChanges { - if fileURI(c.TextDocument.URI) == uri { - edits = append(edits, c.Edits...) + if c.TextDocumentEdit != nil { + if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { + edits = append(edits, c.TextDocumentEdit.Edits...) + } } } } - sedits, err := source.FromProtocolEdits(file.mapper, edits) + newContent, sedits, err := source.ApplyProtocolEdits(file.mapper, edits) if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.ApplyEdits(string(file.mapper.Content), sedits) - filename := file.uri.Filename() switch { case t.Write: @@ -92,8 +92,11 @@ func (t *imports) Run(ctx context.Context, args ...string) error { ioutil.WriteFile(filename, []byte(newContent), 0644) } case t.Diff: - diffs := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) - fmt.Print(diffs) + unified, err := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + if err != nil { + return err + } + fmt.Print(unified) default: fmt.Print(string(newContent)) } diff --git a/internal/lsp/cmd/info.go b/gopls/internal/lsp/cmd/info.go similarity index 98% rename from internal/lsp/cmd/info.go rename to gopls/internal/lsp/cmd/info.go index 8e581a37cb1..68ef40ffb29 100644 --- a/internal/lsp/cmd/info.go +++ b/gopls/internal/lsp/cmd/info.go @@ -14,9 +14,9 @@ import ( "os" "strings" - "golang.org/x/tools/internal/lsp/browser" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/browser" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/links.go b/gopls/internal/lsp/cmd/links.go similarity index 93% rename from internal/lsp/cmd/links.go rename to gopls/internal/lsp/cmd/links.go index 1c48c8c50b9..b5413bba59f 100644 --- a/internal/lsp/cmd/links.go +++ b/gopls/internal/lsp/cmd/links.go @@ -11,8 +11,8 @@ import ( "fmt" "os" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -53,7 +53,7 @@ func (l *links) Run(ctx context.Context, args ...string) error { from := span.Parse(args[0]) uri := from.URI() - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } diff --git a/internal/lsp/cmd/prepare_rename.go b/gopls/internal/lsp/cmd/prepare_rename.go similarity index 94% rename from internal/lsp/cmd/prepare_rename.go rename to gopls/internal/lsp/cmd/prepare_rename.go index 44a192b5be3..e61bd622fe0 100644 --- a/internal/lsp/cmd/prepare_rename.go +++ b/gopls/internal/lsp/cmd/prepare_rename.go @@ -10,8 +10,8 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -51,7 +51,7 @@ func (r *prepareRename) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } diff --git a/internal/lsp/cmd/references.go b/gopls/internal/lsp/cmd/references.go similarity index 92% rename from internal/lsp/cmd/references.go rename to gopls/internal/lsp/cmd/references.go index 0697d2e11b7..2abbb919299 100644 --- a/internal/lsp/cmd/references.go +++ b/gopls/internal/lsp/cmd/references.go @@ -10,8 +10,8 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -51,7 +51,7 @@ func (r *references) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -74,7 +74,7 @@ func (r *references) Run(ctx context.Context, args ...string) error { } var spans []string for _, l := range locations { - f := conn.AddFile(ctx, fileURI(l.URI)) + f := conn.openFile(ctx, fileURI(l.URI)) // convert location to span for user-friendly 1-indexed line // and column numbers span, err := f.mapper.Span(l) diff --git a/internal/lsp/cmd/remote.go b/gopls/internal/lsp/cmd/remote.go similarity index 97% rename from internal/lsp/cmd/remote.go rename to gopls/internal/lsp/cmd/remote.go index 0f4c7216444..684981cfff8 100644 --- a/internal/lsp/cmd/remote.go +++ b/gopls/internal/lsp/cmd/remote.go @@ -13,8 +13,8 @@ import ( "log" "os" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) type remote struct { diff --git a/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go similarity index 80% rename from internal/lsp/cmd/rename.go rename to gopls/internal/lsp/cmd/rename.go index 9411275949f..2cbd260febb 100644 --- a/internal/lsp/cmd/rename.go +++ b/gopls/internal/lsp/cmd/rename.go @@ -13,10 +13,10 @@ import ( "path/filepath" "sort" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/tool" ) @@ -61,7 +61,7 @@ func (r *rename) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } @@ -81,24 +81,24 @@ func (r *rename) Run(ctx context.Context, args ...string) error { var orderedURIs []string edits := map[span.URI][]protocol.TextEdit{} for _, c := range edit.DocumentChanges { - uri := fileURI(c.TextDocument.URI) - edits[uri] = append(edits[uri], c.Edits...) - orderedURIs = append(orderedURIs, string(uri)) + if c.TextDocumentEdit != nil { + uri := fileURI(c.TextDocumentEdit.TextDocument.URI) + edits[uri] = append(edits[uri], c.TextDocumentEdit.Edits...) + orderedURIs = append(orderedURIs, string(uri)) + } } sort.Strings(orderedURIs) changeCount := len(orderedURIs) for _, u := range orderedURIs { uri := span.URIFromURI(u) - cmdFile := conn.AddFile(ctx, uri) + cmdFile := conn.openFile(ctx, uri) filename := cmdFile.uri.Filename() - // convert LSP-style edits to []diff.TextEdit cuz Spans are handy - renameEdits, err := source.FromProtocolEdits(cmdFile.mapper, edits[uri]) + newContent, renameEdits, err := source.ApplyProtocolEdits(cmdFile.mapper, edits[uri]) if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.ApplyEdits(string(cmdFile.mapper.Content), renameEdits) switch { case r.Write: @@ -110,8 +110,11 @@ func (r *rename) Run(ctx context.Context, args ...string) error { } ioutil.WriteFile(filename, []byte(newContent), 0644) case r.Diff: - diffs := diff.ToUnified(filename+".orig", filename, string(cmdFile.mapper.Content), renameEdits) - fmt.Print(diffs) + unified, err := diff.ToUnified(filename+".orig", filename, string(cmdFile.mapper.Content), renameEdits) + if err != nil { + return err + } + fmt.Print(unified) default: if len(orderedURIs) > 1 { fmt.Printf("%s:\n", filepath.Base(filename)) diff --git a/internal/lsp/cmd/semantictokens.go b/gopls/internal/lsp/cmd/semantictokens.go similarity index 96% rename from internal/lsp/cmd/semantictokens.go rename to gopls/internal/lsp/cmd/semantictokens.go index 7dbb7f93c61..3ed08d0248b 100644 --- a/internal/lsp/cmd/semantictokens.go +++ b/gopls/internal/lsp/cmd/semantictokens.go @@ -16,10 +16,10 @@ import ( "os" "unicode/utf8" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) // generate semantic tokens and interpolate them in the file @@ -82,7 +82,7 @@ func (c *semtok) Run(ctx context.Context, args ...string) error { } defer conn.terminate(ctx) uri := span.URIFromPath(args[0]) - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } diff --git a/internal/lsp/cmd/serve.go b/gopls/internal/lsp/cmd/serve.go similarity index 94% rename from internal/lsp/cmd/serve.go rename to gopls/internal/lsp/cmd/serve.go index 1c229a422b4..8a4de5eac6c 100644 --- a/internal/lsp/cmd/serve.go +++ b/gopls/internal/lsp/cmd/serve.go @@ -16,10 +16,10 @@ import ( "golang.org/x/tools/internal/fakenet" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/tool" ) @@ -101,7 +101,7 @@ func (s *Serve) Run(ctx context.Context, args ...string) error { return fmt.Errorf("creating forwarder: %w", err) } } else { - ss = lsprpc.NewStreamServer(cache.New(s.app.options), isDaemon) + ss = lsprpc.NewStreamServer(cache.New(nil, nil, s.app.options), isDaemon) } var network, addr string diff --git a/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go similarity index 93% rename from internal/lsp/cmd/signature.go rename to gopls/internal/lsp/cmd/signature.go index db948430183..77805628ad0 100644 --- a/internal/lsp/cmd/signature.go +++ b/gopls/internal/lsp/cmd/signature.go @@ -9,8 +9,8 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) @@ -46,7 +46,7 @@ func (r *signature) Run(ctx context.Context, args ...string) error { defer conn.terminate(ctx) from := span.Parse(args[0]) - file := conn.AddFile(ctx, from.URI()) + file := conn.openFile(ctx, from.URI()) if file.err != nil { return file.err } diff --git a/internal/lsp/cmd/subcommands.go b/gopls/internal/lsp/cmd/subcommands.go similarity index 100% rename from internal/lsp/cmd/subcommands.go rename to gopls/internal/lsp/cmd/subcommands.go diff --git a/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go similarity index 80% rename from internal/lsp/cmd/suggested_fix.go rename to gopls/internal/lsp/cmd/suggested_fix.go index c6f26e2d685..78310b3b3b9 100644 --- a/internal/lsp/cmd/suggested_fix.go +++ b/gopls/internal/lsp/cmd/suggested_fix.go @@ -10,10 +10,10 @@ import ( "fmt" "io/ioutil" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/tool" ) @@ -56,7 +56,7 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { from := span.Parse(args[0]) uri := from.URI() - file := conn.AddFile(ctx, uri) + file := conn.openFile(ctx, uri) if file.err != nil { return file.err } @@ -103,8 +103,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { } if !from.HasPosition() { for _, c := range a.Edit.DocumentChanges { - if fileURI(c.TextDocument.URI) == uri { - edits = append(edits, c.Edits...) + if c.TextDocumentEdit != nil { + if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { + edits = append(edits, c.TextDocumentEdit.Edits...) + } } } continue @@ -118,8 +120,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { } if span.ComparePoint(from.Start(), spn.Start()) == 0 { for _, c := range a.Edit.DocumentChanges { - if fileURI(c.TextDocument.URI) == uri { - edits = append(edits, c.Edits...) + if c.TextDocumentEdit != nil { + if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { + edits = append(edits, c.TextDocumentEdit.Edits...) + } } } break @@ -129,18 +133,19 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { // If suggested fix is not a diagnostic, still must collect edits. if len(a.Diagnostics) == 0 { for _, c := range a.Edit.DocumentChanges { - if fileURI(c.TextDocument.URI) == uri { - edits = append(edits, c.Edits...) + if c.TextDocumentEdit != nil { + if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { + edits = append(edits, c.TextDocumentEdit.Edits...) + } } } } } - sedits, err := source.FromProtocolEdits(file.mapper, edits) + newContent, sedits, err := source.ApplyProtocolEdits(file.mapper, edits) if err != nil { return fmt.Errorf("%v: %v", edits, err) } - newContent := diff.ApplyEdits(string(file.mapper.Content), sedits) filename := file.uri.Filename() switch { @@ -149,7 +154,10 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { ioutil.WriteFile(filename, []byte(newContent), 0644) } case s.Diff: - diffs := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + diffs, err := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits) + if err != nil { + return err + } fmt.Print(diffs) default: fmt.Print(string(newContent)) diff --git a/internal/lsp/cmd/symbols.go b/gopls/internal/lsp/cmd/symbols.go similarity index 96% rename from internal/lsp/cmd/symbols.go rename to gopls/internal/lsp/cmd/symbols.go index b43a6dcd1f7..3ecdff8011c 100644 --- a/internal/lsp/cmd/symbols.go +++ b/gopls/internal/lsp/cmd/symbols.go @@ -11,8 +11,8 @@ import ( "fmt" "sort" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/tool" ) diff --git a/internal/lsp/cmd/test/call_hierarchy.go b/gopls/internal/lsp/cmd/test/call_hierarchy.go similarity index 95% rename from internal/lsp/cmd/test/call_hierarchy.go rename to gopls/internal/lsp/cmd/test/call_hierarchy.go index 38f8ed707a4..bb8d306224a 100644 --- a/internal/lsp/cmd/test/call_hierarchy.go +++ b/gopls/internal/lsp/cmd/test/call_hierarchy.go @@ -10,9 +10,9 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) { diff --git a/gopls/internal/lsp/cmd/test/check.go b/gopls/internal/lsp/cmd/test/check.go new file mode 100644 index 00000000000..ea9747cae79 --- /dev/null +++ b/gopls/internal/lsp/cmd/test/check.go @@ -0,0 +1,63 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmdtest + +import ( + "io/ioutil" + "strings" + "testing" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" +) + +// Diagnostics runs the gopls command on a single file, parses its +// diagnostics, and compares against the expectations defined by +// markers in the source file. +func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) { + out, _ := r.runGoplsCmd(t, "check", uri.Filename()) + + content, err := ioutil.ReadFile(uri.Filename()) + if err != nil { + t.Fatal(err) + } + mapper := protocol.NewColumnMapper(uri, content) + + // Parse command output into a set of diagnostics. + var got []*source.Diagnostic + for _, line := range strings.Split(out, "\n") { + if line == "" { + continue // skip blank + } + parts := strings.SplitN(line, ": ", 2) // "span: message" + if len(parts) != 2 { + t.Fatalf("output line not of form 'span: message': %q", line) + } + spn, message := span.Parse(parts[0]), parts[1] + rng, err := mapper.Range(spn) + if err != nil { + t.Fatal(err) + } + // Set only the fields needed by DiffDiagnostics. + got = append(got, &source.Diagnostic{ + URI: uri, + Range: rng, + Message: message, + }) + } + + // Don't expect fields that we can't populate from the command output. + for _, diag := range want { + if diag.Source == "no_diagnostics" { + continue // see DiffDiagnostics + } + diag.Source = "" + diag.Severity = 0 + } + + tests.CompareDiagnostics(t, uri, want, got) +} diff --git a/internal/lsp/cmd/test/cmdtest.go b/gopls/internal/lsp/cmd/test/cmdtest.go similarity index 88% rename from internal/lsp/cmd/test/cmdtest.go rename to gopls/internal/lsp/cmd/test/cmdtest.go index 312f7b8b435..167631a3cab 100644 --- a/internal/lsp/cmd/test/cmdtest.go +++ b/gopls/internal/lsp/cmd/test/cmdtest.go @@ -12,18 +12,19 @@ import ( "fmt" "io" "os" + "runtime" "sync" "testing" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/cmd" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/tool" ) @@ -37,8 +38,8 @@ type runner struct { func TestCommandLine(t *testing.T, testdata string, options func(*source.Options)) { // On Android, the testdata directory is not copied to the runner. - if stat, err := os.Stat(testdata); err != nil || !stat.IsDir() { - t.Skip("testdata directory not present") + if runtime.GOOS == "android" { + t.Skip("testdata directory not present on android") } tests.RunTests(t, testdata, false, func(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) @@ -50,7 +51,7 @@ func TestCommandLine(t *testing.T, testdata string, options func(*source.Options func NewTestServer(ctx context.Context, options func(*source.Options)) *servertest.TCPServer { ctx = debug.WithInstance(ctx, "", "") - cache := cache.New(options) + cache := cache.New(nil, nil, options) ss := lsprpc.NewStreamServer(cache, false) return servertest.NewTCPServer(ctx, ss, nil) } @@ -113,6 +114,10 @@ func (r *runner) Hover(t *testing.T, spn span.Span, info string) { //TODO: hovering not supported on command line } +func (r *runner) InlayHints(t *testing.T, spn span.Span) { + // TODO: inlayHints not supported on command line +} + func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) { rStdout, wStdout, err := os.Pipe() if err != nil { diff --git a/internal/lsp/cmd/test/definition.go b/gopls/internal/lsp/cmd/test/definition.go similarity index 71% rename from internal/lsp/cmd/test/definition.go rename to gopls/internal/lsp/cmd/test/definition.go index c82d9a6c1ae..ca84e80ebe2 100644 --- a/internal/lsp/cmd/test/definition.go +++ b/gopls/internal/lsp/cmd/test/definition.go @@ -10,10 +10,8 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" ) type godefMode int @@ -47,15 +45,11 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if mode&jsonGoDef != 0 && runtime.GOOS == "windows" { got = strings.Replace(got, "file:///", "file://", -1) } - expect := strings.TrimSpace(string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + expect := strings.TrimSpace(string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil }))) if expect != "" && !strings.HasPrefix(got, expect) { - d, err := myers.ComputeEdits("", expect, got) - if err != nil { - t.Fatal(err) - } - t.Errorf("definition %v failed with %#v\n%s", tag, args, diff.ToUnified("expect", "got", expect, d)) + tests.CheckSameMarkdown(t, got, expect) } } } diff --git a/internal/lsp/cmd/test/folding_range.go b/gopls/internal/lsp/cmd/test/folding_range.go similarity index 81% rename from internal/lsp/cmd/test/folding_range.go rename to gopls/internal/lsp/cmd/test/folding_range.go index 4478687b549..184c01a05bb 100644 --- a/internal/lsp/cmd/test/folding_range.go +++ b/gopls/internal/lsp/cmd/test/folding_range.go @@ -7,7 +7,7 @@ package cmdtest import ( "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { @@ -15,7 +15,7 @@ func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { uri := spn.URI() filename := uri.Filename() got, _ := r.NormalizeGoplsCmd(t, "folding_ranges", filename) - expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { return []byte(got), nil })) diff --git a/internal/lsp/cmd/test/format.go b/gopls/internal/lsp/cmd/test/format.go similarity index 95% rename from internal/lsp/cmd/test/format.go rename to gopls/internal/lsp/cmd/test/format.go index 77eedd440e4..368d535b20a 100644 --- a/internal/lsp/cmd/test/format.go +++ b/gopls/internal/lsp/cmd/test/format.go @@ -6,14 +6,15 @@ package cmdtest import ( "bytes" - exec "golang.org/x/sys/execabs" "io/ioutil" "os" "regexp" "strings" "testing" - "golang.org/x/tools/internal/span" + exec "golang.org/x/sys/execabs" + + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -21,7 +22,7 @@ func (r *runner) Format(t *testing.T, spn span.Span) { tag := "gofmt" uri := spn.URI() filename := uri.Filename() - expect := string(r.data.Golden(tag, filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, tag, filename, func() ([]byte, error) { cmd := exec.Command("gofmt", filename) contents, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files contents = []byte(r.Normalize(fixFileHeader(string(contents)))) diff --git a/internal/lsp/cmd/test/highlight.go b/gopls/internal/lsp/cmd/test/highlight.go similarity index 94% rename from internal/lsp/cmd/test/highlight.go rename to gopls/internal/lsp/cmd/test/highlight.go index 99e8b2c3fc7..cd51b093c68 100644 --- a/internal/lsp/cmd/test/highlight.go +++ b/gopls/internal/lsp/cmd/test/highlight.go @@ -9,7 +9,7 @@ import ( "fmt" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Highlight(t *testing.T, spn span.Span, spans []span.Span) { diff --git a/internal/lsp/cmd/test/implementation.go b/gopls/internal/lsp/cmd/test/implementation.go similarity index 95% rename from internal/lsp/cmd/test/implementation.go rename to gopls/internal/lsp/cmd/test/implementation.go index 189452466ce..e24584da99d 100644 --- a/internal/lsp/cmd/test/implementation.go +++ b/gopls/internal/lsp/cmd/test/implementation.go @@ -9,7 +9,7 @@ import ( "sort" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Implementation(t *testing.T, spn span.Span, imps []span.Span) { diff --git a/internal/lsp/cmd/test/imports.go b/gopls/internal/lsp/cmd/test/imports.go similarity index 51% rename from internal/lsp/cmd/test/imports.go rename to gopls/internal/lsp/cmd/test/imports.go index ce8aee55dfa..d26c88664e2 100644 --- a/internal/lsp/cmd/test/imports.go +++ b/gopls/internal/lsp/cmd/test/imports.go @@ -7,23 +7,19 @@ package cmdtest import ( "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/diff" ) func (r *runner) Import(t *testing.T, spn span.Span) { uri := spn.URI() filename := uri.Filename() got, _ := r.NormalizeGoplsCmd(t, "imports", filename) - want := string(r.data.Golden("goimports", filename, func() ([]byte, error) { + want := string(r.data.Golden(t, "goimports", filename, func() ([]byte, error) { return []byte(got), nil })) if want != got { - d, err := myers.ComputeEdits(uri, want, got) - if err != nil { - t.Fatal(err) - } - t.Errorf("imports failed for %s, expected:\n%s", filename, diff.ToUnified("want", "got", want, d)) + unified := diff.Unified("want", "got", want, got) + t.Errorf("imports failed for %s, expected:\n%s", filename, unified) } } diff --git a/internal/lsp/cmd/test/links.go b/gopls/internal/lsp/cmd/test/links.go similarity index 81% rename from internal/lsp/cmd/test/links.go rename to gopls/internal/lsp/cmd/test/links.go index 88df768323a..a9616ee48a9 100644 --- a/internal/lsp/cmd/test/links.go +++ b/gopls/internal/lsp/cmd/test/links.go @@ -8,9 +8,9 @@ import ( "encoding/json" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { diff --git a/internal/lsp/cmd/test/prepare_rename.go b/gopls/internal/lsp/cmd/test/prepare_rename.go similarity index 85% rename from internal/lsp/cmd/test/prepare_rename.go rename to gopls/internal/lsp/cmd/test/prepare_rename.go index b5359e57b42..c818c0197da 100644 --- a/internal/lsp/cmd/test/prepare_rename.go +++ b/gopls/internal/lsp/cmd/test/prepare_rename.go @@ -8,10 +8,10 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/cmd" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { diff --git a/internal/lsp/cmd/test/references.go b/gopls/internal/lsp/cmd/test/references.go similarity index 97% rename from internal/lsp/cmd/test/references.go rename to gopls/internal/lsp/cmd/test/references.go index 66d0d066286..85c9bc84a62 100644 --- a/internal/lsp/cmd/test/references.go +++ b/gopls/internal/lsp/cmd/test/references.go @@ -9,7 +9,7 @@ import ( "sort" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) References(t *testing.T, spn span.Span, itemList []span.Span) { diff --git a/internal/lsp/cmd/test/rename.go b/gopls/internal/lsp/cmd/test/rename.go similarity index 63% rename from internal/lsp/cmd/test/rename.go rename to gopls/internal/lsp/cmd/test/rename.go index 0fe2d1e1825..a9eb31e3877 100644 --- a/internal/lsp/cmd/test/rename.go +++ b/gopls/internal/lsp/cmd/test/rename.go @@ -8,7 +8,8 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { @@ -17,13 +18,13 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { loc := fmt.Sprintf("%v", spn) got, err := r.NormalizeGoplsCmd(t, "rename", loc, newText) got += err - expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) { + want := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { return []byte(got), nil })) - if expect != got { - t.Errorf("rename failed with %v %v\nexpected:\n%s\ngot:\n%s", loc, newText, expect, got) + if diff := compare.Text(want, got); diff != "" { + t.Errorf("rename failed with %v %v (-want +got):\n%s", loc, newText, diff) } // now check we can build a valid unified diff unified, _ := r.NormalizeGoplsCmd(t, "rename", "-d", loc, newText) - checkUnified(t, filename, expect, unified) + checkUnified(t, filename, want, unified) } diff --git a/internal/lsp/cmd/test/semanticdriver.go b/gopls/internal/lsp/cmd/test/semanticdriver.go similarity index 88% rename from internal/lsp/cmd/test/semanticdriver.go rename to gopls/internal/lsp/cmd/test/semanticdriver.go index 247f755bf20..069dd64f6e6 100644 --- a/internal/lsp/cmd/test/semanticdriver.go +++ b/gopls/internal/lsp/cmd/test/semanticdriver.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { @@ -18,7 +18,7 @@ func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { if stderr != "" { t.Fatalf("%s: %q", filename, stderr) } - want := string(r.data.Golden("semantic", filename, func() ([]byte, error) { + want := string(r.data.Golden(t, "semantic", filename, func() ([]byte, error) { return []byte(got), nil })) if want != got { diff --git a/internal/lsp/cmd/test/signature.go b/gopls/internal/lsp/cmd/test/signature.go similarity index 78% rename from internal/lsp/cmd/test/signature.go rename to gopls/internal/lsp/cmd/test/signature.go index f6bdaebf312..40669e8d223 100644 --- a/internal/lsp/cmd/test/signature.go +++ b/gopls/internal/lsp/cmd/test/signature.go @@ -8,9 +8,9 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) { @@ -25,7 +25,7 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa return } goldenTag := want.Signatures[0].Label + "-signature" - expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, goldenTag, filename, func() ([]byte, error) { return []byte(got), nil })) if tests.NormalizeAny(expect) != tests.NormalizeAny(got) { diff --git a/internal/lsp/cmd/test/suggested_fix.go b/gopls/internal/lsp/cmd/test/suggested_fix.go similarity index 52% rename from internal/lsp/cmd/test/suggested_fix.go rename to gopls/internal/lsp/cmd/test/suggested_fix.go index c819e051735..1e61fe9bcd5 100644 --- a/internal/lsp/cmd/test/suggested_fix.go +++ b/gopls/internal/lsp/cmd/test/suggested_fix.go @@ -8,28 +8,31 @@ import ( "fmt" "testing" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" ) -func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, expectedActions int) { +func (r *runner) SuggestedFix(t *testing.T, spn span.Span, suggestedFixes []tests.SuggestedFix, expectedActions int) { uri := spn.URI() filename := uri.Filename() args := []string{"fix", "-a", fmt.Sprintf("%s", spn)} - for _, kind := range actionKinds { - if kind == "refactor.rewrite" { + var actionKinds []string + for _, sf := range suggestedFixes { + if sf.ActionKind == "refactor.rewrite" { t.Skip("refactor.rewrite is not yet supported on the command line") } + actionKinds = append(actionKinds, sf.ActionKind) } args = append(args, actionKinds...) got, stderr := r.NormalizeGoplsCmd(t, args...) if stderr == "ExecuteCommand is not yet supported on the command line" { return // don't skip to keep the summary counts correct } - want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), filename, func() ([]byte, error) { + want := string(r.data.Golden(t, "suggestedfix_"+tests.SpanName(spn), filename, func() ([]byte, error) { return []byte(got), nil })) if want != got { - t.Errorf("suggested fixes failed for %s:\n%s", filename, tests.Diff(t, want, got)) + t.Errorf("suggested fixes failed for %s:\n%s", filename, compare.Text(want, got)) } } diff --git a/internal/lsp/cmd/test/symbols.go b/gopls/internal/lsp/cmd/test/symbols.go similarity index 55% rename from internal/lsp/cmd/test/symbols.go rename to gopls/internal/lsp/cmd/test/symbols.go index 055be030829..aaf3725d9c0 100644 --- a/internal/lsp/cmd/test/symbols.go +++ b/gopls/internal/lsp/cmd/test/symbols.go @@ -7,17 +7,18 @@ package cmdtest import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { filename := uri.Filename() got, _ := r.NormalizeGoplsCmd(t, "symbols", filename) - expect := string(r.data.Golden("symbols", filename, func() ([]byte, error) { + expect := string(r.data.Golden(t, "symbols", filename, func() ([]byte, error) { return []byte(got), nil })) - if expect != got { - t.Errorf("symbols failed for %s expected:\n%s\ngot:\n%s", filename, expect, got) + if diff := compare.Text(expect, got); diff != "" { + t.Errorf("symbols differ from expected:\n%s", diff) } } diff --git a/internal/lsp/cmd/test/workspace_symbol.go b/gopls/internal/lsp/cmd/test/workspace_symbol.go similarity index 72% rename from internal/lsp/cmd/test/workspace_symbol.go rename to gopls/internal/lsp/cmd/test/workspace_symbol.go index ce965f03a31..40c2c65d019 100644 --- a/internal/lsp/cmd/test/workspace_symbol.go +++ b/gopls/internal/lsp/cmd/test/workspace_symbol.go @@ -11,9 +11,10 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { @@ -43,11 +44,11 @@ func (r *runner) runWorkspaceSymbols(t *testing.T, uri span.URI, matcher, query sort.Strings(filtered) got := r.Normalize(strings.Join(filtered, "\n") + "\n") - expect := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { + expect := string(r.data.Golden(t, fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) if expect != got { - t.Errorf("workspace_symbol failed for %s:\n%s", query, tests.Diff(t, expect, got)) + t.Errorf("workspace_symbol failed for %s:\n%s", query, compare.Text(expect, got)) } } diff --git a/internal/lsp/cmd/usage/api-json.hlp b/gopls/internal/lsp/cmd/usage/api-json.hlp similarity index 100% rename from internal/lsp/cmd/usage/api-json.hlp rename to gopls/internal/lsp/cmd/usage/api-json.hlp diff --git a/internal/lsp/cmd/usage/bug.hlp b/gopls/internal/lsp/cmd/usage/bug.hlp similarity index 100% rename from internal/lsp/cmd/usage/bug.hlp rename to gopls/internal/lsp/cmd/usage/bug.hlp diff --git a/internal/lsp/cmd/usage/call_hierarchy.hlp b/gopls/internal/lsp/cmd/usage/call_hierarchy.hlp similarity index 100% rename from internal/lsp/cmd/usage/call_hierarchy.hlp rename to gopls/internal/lsp/cmd/usage/call_hierarchy.hlp diff --git a/internal/lsp/cmd/usage/check.hlp b/gopls/internal/lsp/cmd/usage/check.hlp similarity index 100% rename from internal/lsp/cmd/usage/check.hlp rename to gopls/internal/lsp/cmd/usage/check.hlp diff --git a/internal/lsp/cmd/usage/definition.hlp b/gopls/internal/lsp/cmd/usage/definition.hlp similarity index 100% rename from internal/lsp/cmd/usage/definition.hlp rename to gopls/internal/lsp/cmd/usage/definition.hlp diff --git a/internal/lsp/cmd/usage/fix.hlp b/gopls/internal/lsp/cmd/usage/fix.hlp similarity index 100% rename from internal/lsp/cmd/usage/fix.hlp rename to gopls/internal/lsp/cmd/usage/fix.hlp diff --git a/internal/lsp/cmd/usage/folding_ranges.hlp b/gopls/internal/lsp/cmd/usage/folding_ranges.hlp similarity index 100% rename from internal/lsp/cmd/usage/folding_ranges.hlp rename to gopls/internal/lsp/cmd/usage/folding_ranges.hlp diff --git a/internal/lsp/cmd/usage/format.hlp b/gopls/internal/lsp/cmd/usage/format.hlp similarity index 100% rename from internal/lsp/cmd/usage/format.hlp rename to gopls/internal/lsp/cmd/usage/format.hlp diff --git a/internal/lsp/cmd/usage/help.hlp b/gopls/internal/lsp/cmd/usage/help.hlp similarity index 100% rename from internal/lsp/cmd/usage/help.hlp rename to gopls/internal/lsp/cmd/usage/help.hlp diff --git a/internal/lsp/cmd/usage/highlight.hlp b/gopls/internal/lsp/cmd/usage/highlight.hlp similarity index 100% rename from internal/lsp/cmd/usage/highlight.hlp rename to gopls/internal/lsp/cmd/usage/highlight.hlp diff --git a/internal/lsp/cmd/usage/implementation.hlp b/gopls/internal/lsp/cmd/usage/implementation.hlp similarity index 100% rename from internal/lsp/cmd/usage/implementation.hlp rename to gopls/internal/lsp/cmd/usage/implementation.hlp diff --git a/internal/lsp/cmd/usage/imports.hlp b/gopls/internal/lsp/cmd/usage/imports.hlp similarity index 100% rename from internal/lsp/cmd/usage/imports.hlp rename to gopls/internal/lsp/cmd/usage/imports.hlp diff --git a/internal/lsp/cmd/usage/inspect.hlp b/gopls/internal/lsp/cmd/usage/inspect.hlp similarity index 100% rename from internal/lsp/cmd/usage/inspect.hlp rename to gopls/internal/lsp/cmd/usage/inspect.hlp diff --git a/internal/lsp/cmd/usage/licenses.hlp b/gopls/internal/lsp/cmd/usage/licenses.hlp similarity index 100% rename from internal/lsp/cmd/usage/licenses.hlp rename to gopls/internal/lsp/cmd/usage/licenses.hlp diff --git a/internal/lsp/cmd/usage/links.hlp b/gopls/internal/lsp/cmd/usage/links.hlp similarity index 100% rename from internal/lsp/cmd/usage/links.hlp rename to gopls/internal/lsp/cmd/usage/links.hlp diff --git a/internal/lsp/cmd/usage/prepare_rename.hlp b/gopls/internal/lsp/cmd/usage/prepare_rename.hlp similarity index 100% rename from internal/lsp/cmd/usage/prepare_rename.hlp rename to gopls/internal/lsp/cmd/usage/prepare_rename.hlp diff --git a/internal/lsp/cmd/usage/references.hlp b/gopls/internal/lsp/cmd/usage/references.hlp similarity index 100% rename from internal/lsp/cmd/usage/references.hlp rename to gopls/internal/lsp/cmd/usage/references.hlp diff --git a/internal/lsp/cmd/usage/remote.hlp b/gopls/internal/lsp/cmd/usage/remote.hlp similarity index 100% rename from internal/lsp/cmd/usage/remote.hlp rename to gopls/internal/lsp/cmd/usage/remote.hlp diff --git a/internal/lsp/cmd/usage/rename.hlp b/gopls/internal/lsp/cmd/usage/rename.hlp similarity index 100% rename from internal/lsp/cmd/usage/rename.hlp rename to gopls/internal/lsp/cmd/usage/rename.hlp diff --git a/internal/lsp/cmd/usage/semtok.hlp b/gopls/internal/lsp/cmd/usage/semtok.hlp similarity index 100% rename from internal/lsp/cmd/usage/semtok.hlp rename to gopls/internal/lsp/cmd/usage/semtok.hlp diff --git a/internal/lsp/cmd/usage/serve.hlp b/gopls/internal/lsp/cmd/usage/serve.hlp similarity index 100% rename from internal/lsp/cmd/usage/serve.hlp rename to gopls/internal/lsp/cmd/usage/serve.hlp diff --git a/internal/lsp/cmd/usage/signature.hlp b/gopls/internal/lsp/cmd/usage/signature.hlp similarity index 100% rename from internal/lsp/cmd/usage/signature.hlp rename to gopls/internal/lsp/cmd/usage/signature.hlp diff --git a/internal/lsp/cmd/usage/symbols.hlp b/gopls/internal/lsp/cmd/usage/symbols.hlp similarity index 100% rename from internal/lsp/cmd/usage/symbols.hlp rename to gopls/internal/lsp/cmd/usage/symbols.hlp diff --git a/internal/lsp/cmd/usage/usage.hlp b/gopls/internal/lsp/cmd/usage/usage.hlp similarity index 100% rename from internal/lsp/cmd/usage/usage.hlp rename to gopls/internal/lsp/cmd/usage/usage.hlp diff --git a/internal/lsp/cmd/usage/version.hlp b/gopls/internal/lsp/cmd/usage/version.hlp similarity index 100% rename from internal/lsp/cmd/usage/version.hlp rename to gopls/internal/lsp/cmd/usage/version.hlp diff --git a/internal/lsp/cmd/usage/vulncheck.hlp b/gopls/internal/lsp/cmd/usage/vulncheck.hlp similarity index 70% rename from internal/lsp/cmd/usage/vulncheck.hlp rename to gopls/internal/lsp/cmd/usage/vulncheck.hlp index 19a674b2ea7..4fbe573e22a 100644 --- a/internal/lsp/cmd/usage/vulncheck.hlp +++ b/gopls/internal/lsp/cmd/usage/vulncheck.hlp @@ -6,10 +6,12 @@ Usage: WARNING: this command is experimental. By default, the command outputs a JSON-encoded - golang.org/x/tools/internal/lsp/command.VulncheckResult + golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult message. Example: $ gopls vulncheck -config If true, the command reads a JSON-encoded package load configuration from stdin + -summary + If true, outputs a JSON-encoded govulnchecklib.Summary JSON diff --git a/internal/lsp/cmd/usage/workspace.hlp b/gopls/internal/lsp/cmd/usage/workspace.hlp similarity index 100% rename from internal/lsp/cmd/usage/workspace.hlp rename to gopls/internal/lsp/cmd/usage/workspace.hlp diff --git a/internal/lsp/cmd/usage/workspace_symbol.hlp b/gopls/internal/lsp/cmd/usage/workspace_symbol.hlp similarity index 100% rename from internal/lsp/cmd/usage/workspace_symbol.hlp rename to gopls/internal/lsp/cmd/usage/workspace_symbol.hlp diff --git a/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go similarity index 62% rename from internal/lsp/cmd/vulncheck.go rename to gopls/internal/lsp/cmd/vulncheck.go index 4d245cecb60..93b9c106aef 100644 --- a/internal/lsp/cmd/vulncheck.go +++ b/gopls/internal/lsp/cmd/vulncheck.go @@ -12,16 +12,15 @@ import ( "os" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + vulnchecklib "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/tool" ) // vulncheck implements the vulncheck command. type vulncheck struct { - Config bool `flag:"config" help:"If true, the command reads a JSON-encoded package load configuration from stdin"` - app *Application + Config bool `flag:"config" help:"If true, the command reads a JSON-encoded package load configuration from stdin"` + AsSummary bool `flag:"summary" help:"If true, outputs a JSON-encoded govulnchecklib.Summary JSON"` + app *Application } type pkgLoadConfig struct { @@ -29,10 +28,6 @@ type pkgLoadConfig struct { // the build system's query tool. BuildFlags []string - // Env is the environment to use when invoking the build system's query tool. - // If Env is nil, the current environment is used. - Env []string - // If Tests is set, the loader includes related test packages. Tests bool } @@ -50,7 +45,7 @@ func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { WARNING: this command is experimental. By default, the command outputs a JSON-encoded - golang.org/x/tools/internal/lsp/command.VulncheckResult + golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult message. Example: $ gopls vulncheck @@ -60,6 +55,11 @@ func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { } func (v *vulncheck) Run(ctx context.Context, args ...string) error { + if vulnchecklib.Govulncheck == nil { + return fmt.Errorf("vulncheck command is available only in gopls compiled with go1.18 or newer") + } + + // TODO(hyangah): what's wrong with allowing multiple targets? if len(args) > 1 { return tool.CommandLineErrorf("vulncheck accepts at most one package pattern") } @@ -68,40 +68,32 @@ func (v *vulncheck) Run(ctx context.Context, args ...string) error { pattern = args[0] } - cwd, err := os.Getwd() - if err != nil { - return tool.CommandLineErrorf("failed to get current directory: %v", err) - } var cfg pkgLoadConfig if v.Config { if err := json.NewDecoder(os.Stdin).Decode(&cfg); err != nil { return tool.CommandLineErrorf("failed to parse cfg: %v", err) } } - - opts := source.DefaultOptions().Clone() - v.app.options(opts) // register hook - if opts == nil || opts.Hooks.Govulncheck == nil { - return tool.CommandLineErrorf("vulncheck feature is not available") - } - - loadCfg := &packages.Config{ + loadCfg := packages.Config{ Context: ctx, Tests: cfg.Tests, BuildFlags: cfg.BuildFlags, - Env: cfg.Env, + // inherit the current process's cwd and env. } - res, err := opts.Hooks.Govulncheck(ctx, loadCfg, command.VulncheckArgs{ - Dir: protocol.URIFromPath(cwd), - Pattern: pattern, - }) + if v.AsSummary { + // vulnchecklib.Main calls os.Exit and never returns. + vulnchecklib.Main(loadCfg, args...) + return nil + } + // TODO(hyangah): delete. + res, err := vulnchecklib.Govulncheck(ctx, &loadCfg, pattern) if err != nil { - return tool.CommandLineErrorf("govulncheck failed: %v", err) + return fmt.Errorf("vulncheck failed: %v", err) } data, err := json.MarshalIndent(res, " ", " ") if err != nil { - return tool.CommandLineErrorf("failed to decode results: %v", err) + return fmt.Errorf("vulncheck failed to encode result: %v", err) } fmt.Printf("%s", data) return nil diff --git a/internal/lsp/cmd/workspace.go b/gopls/internal/lsp/cmd/workspace.go similarity index 93% rename from internal/lsp/cmd/workspace.go rename to gopls/internal/lsp/cmd/workspace.go index c0ddd9eb46e..2038d276348 100644 --- a/internal/lsp/cmd/workspace.go +++ b/gopls/internal/lsp/cmd/workspace.go @@ -9,9 +9,9 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) // workspace is a top-level command for working with the gopls workspace. This diff --git a/internal/lsp/cmd/workspace_symbol.go b/gopls/internal/lsp/cmd/workspace_symbol.go similarity index 93% rename from internal/lsp/cmd/workspace_symbol.go rename to gopls/internal/lsp/cmd/workspace_symbol.go index 38fe5decf7f..be1e24ef324 100644 --- a/internal/lsp/cmd/workspace_symbol.go +++ b/gopls/internal/lsp/cmd/workspace_symbol.go @@ -9,8 +9,8 @@ import ( "flag" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/tool" ) @@ -73,7 +73,7 @@ func (r *workspaceSymbol) Run(ctx context.Context, args ...string) error { return err } for _, s := range symbols { - f := conn.AddFile(ctx, fileURI(s.Location.URI)) + f := conn.openFile(ctx, fileURI(s.Location.URI)) span, err := f.mapper.Span(s.Location) if err != nil { return err diff --git a/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go similarity index 84% rename from internal/lsp/code_action.go rename to gopls/internal/lsp/code_action.go index 9d78e3c9ac9..d19cafc0704 100644 --- a/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -10,14 +10,14 @@ import ( "sort" "strings" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" ) func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { @@ -70,18 +70,41 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara switch kind { case source.Mod: if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 { - diags, err := mod.DiagnosticsForMod(ctx, snapshot, fh) + diags, err := mod.ModDiagnostics(ctx, snapshot, fh) if source.IsNonFatalGoModError(err) { return nil, nil } if err != nil { return nil, err } - quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, diags) + udiags, err := mod.ModUpgradeDiagnostics(ctx, snapshot, fh) + if err != nil { + return nil, err + } + quickFixes, err := codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, append(diags, udiags...)) if err != nil { return nil, err } codeActions = append(codeActions, quickFixes...) + + vdiags, err := mod.ModVulnerabilityDiagnostics(ctx, snapshot, fh) + if err != nil { + return nil, err + } + // Group vulnerabilities by location and then limit which code actions we return + // for each location. + m := make(map[protocol.Range][]*source.Diagnostic) + for _, v := range vdiags { + m[v.Range] = append(m[v.Range], v) + } + for _, sdiags := range m { + quickFixes, err = codeActionsMatchingDiagnostics(ctx, snapshot, diagnostics, sdiags) + if err != nil { + return nil, err + } + quickFixes = mod.SelectUpgradeCodeActions(quickFixes) + codeActions = append(codeActions, quickFixes...) + } } case source.Go: // Don't suggest fixes for generated files, since they are generally @@ -257,6 +280,12 @@ func importDiagnostics(fix *imports.ImportFix, diagnostics []protocol.Diagnostic if ident == fix.IdentName { results = append(results, diagnostic) } + // "undefined: X" may be an unresolved import at Go 1.20+. + case strings.HasPrefix(diagnostic.Message, "undefined: "): + ident := strings.TrimPrefix(diagnostic.Message, "undefined: ") + if ident == fix.IdentName { + results = append(results, diagnostic) + } // "could not import: X" may be an invalid import. case strings.HasPrefix(diagnostic.Message, "could not import: "): ident := strings.TrimPrefix(diagnostic.Message, "could not import: ") @@ -294,7 +323,7 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.P } puri := protocol.URIFromSpanURI(uri) var commands []protocol.Command - if _, ok, methodOk, _ := source.CanExtractFunction(snapshot.FileSet(), srng, pgf.Src, pgf.File); ok { + if _, ok, methodOk, _ := source.CanExtractFunction(pgf.Tok, srng, pgf.Src, pgf.File); ok { cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ URI: puri, Fix: source.ExtractFunction, @@ -338,16 +367,18 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.P return actions, nil } -func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.TextDocumentEdit { - return []protocol.TextDocumentEdit{ +func documentChanges(fh source.VersionedFileHandle, edits []protocol.TextEdit) []protocol.DocumentChanges { + return []protocol.DocumentChanges{ { - TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ - Version: fh.Version(), - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(fh.URI()), + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + Version: fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(fh.URI()), + }, }, + Edits: edits, }, - Edits: edits, }, } } @@ -378,20 +409,22 @@ func codeActionsMatchingDiagnostics(ctx context.Context, snapshot source.Snapsho func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd *source.Diagnostic, pd *protocol.Diagnostic) ([]protocol.CodeAction, error) { var actions []protocol.CodeAction for _, fix := range sd.SuggestedFixes { - var changes []protocol.TextDocumentEdit + var changes []protocol.DocumentChanges for uri, edits := range fix.Edits { fh, err := snapshot.GetVersionedFile(ctx, uri) if err != nil { return nil, err } - changes = append(changes, protocol.TextDocumentEdit{ - TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ - Version: fh.Version(), - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(uri), + changes = append(changes, protocol.DocumentChanges{ + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + Version: fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(fh.URI()), + }, }, + Edits: edits, }, - Edits: edits, }) } action := protocol.CodeAction{ @@ -411,7 +444,8 @@ func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd } func sameDiagnostic(pd protocol.Diagnostic, sd *source.Diagnostic) bool { - return pd.Message == sd.Message && protocol.CompareRange(pd.Range, sd.Range) == 0 && pd.Source == string(sd.Source) + return 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) } func goTest(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) { diff --git a/internal/lsp/code_lens.go b/gopls/internal/lsp/code_lens.go similarity index 88% rename from internal/lsp/code_lens.go rename to gopls/internal/lsp/code_lens.go index e1944583883..4bbe2bb34c6 100644 --- a/internal/lsp/code_lens.go +++ b/gopls/internal/lsp/code_lens.go @@ -10,10 +10,10 @@ import ( "sort" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) { diff --git a/internal/lsp/command.go b/gopls/internal/lsp/command.go similarity index 82% rename from internal/lsp/command.go rename to gopls/internal/lsp/command.go index 862af6088ec..e231b4bba4e 100644 --- a/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -13,21 +13,23 @@ import ( "io" "io/ioutil" "os" + "os/exec" "path/filepath" "sort" "strings" "golang.org/x/mod/modfile" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/govulncheck" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/progress" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/progress" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -144,9 +146,15 @@ func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs if err != nil { return err } + var changes []protocol.DocumentChanges + for _, edit := range edits { + changes = append(changes, protocol.DocumentChanges{ + TextDocumentEdit: &edit, + }) + } r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ Edit: protocol.WorkspaceEdit{ - DocumentChanges: edits, + DocumentChanges: changes, }, }) if err != nil { @@ -180,7 +188,7 @@ func (c *commandHandler) CheckUpgrades(ctx context.Context, args command.CheckUp if err != nil { return err } - deps.snapshot.View().RegisterModuleUpgrades(upgrades) + deps.snapshot.View().RegisterModuleUpgrades(args.URI.SpanURI(), upgrades) // Re-diagnose the snapshot to publish the new module diagnostics. c.s.diagnoseSnapshot(deps.snapshot, nil, false) return nil @@ -195,6 +203,24 @@ func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.Dep return c.GoGetModule(ctx, args) } +func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, uri command.URIArg) error { + return c.run(ctx, commandConfig{ + forURI: uri.URI, + }, func(ctx context.Context, deps commandDeps) error { + deps.snapshot.View().ClearModuleUpgrades(uri.URI.SpanURI()) + deps.snapshot.View().SetVulnerabilities(uri.URI.SpanURI(), nil) + // Clear all diagnostics coming from the upgrade check source and vulncheck. + // This will clear the diagnostics in all go.mod files, but they + // will be re-calculated when the snapshot is diagnosed again. + c.s.clearDiagnosticSource(modCheckUpgradesSource) + c.s.clearDiagnosticSource(modVulncheckSource) + + // Re-diagnose the snapshot to remove the diagnostics. + c.s.diagnoseSnapshot(deps.snapshot, nil, false) + return nil + }) +} + func (c *commandHandler) GoGetModule(ctx context.Context, args command.DependencyArgs) error { return c.run(ctx, commandConfig{ progress: "Running go get", @@ -322,15 +348,19 @@ func (c *commandHandler) RemoveDependency(ctx context.Context, args command.Remo } response, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ Edit: protocol.WorkspaceEdit{ - DocumentChanges: []protocol.TextDocumentEdit{{ - TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ - Version: deps.fh.Version(), - TextDocumentIdentifier: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(deps.fh.URI()), + DocumentChanges: []protocol.DocumentChanges{ + { + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + Version: deps.fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(deps.fh.URI()), + }, + }, + Edits: edits, }, }, - Edits: edits, - }}, + }, }, }) if err != nil { @@ -361,10 +391,7 @@ func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePat return nil, err } // Calculate the edits to be made due to the change. - diff, err := snapshot.View().Options().ComputeEdits(pm.URI, string(pm.Mapper.Content), string(newContent)) - if err != nil { - return nil, err - } + diff := snapshot.View().Options().ComputeEdits(string(pm.Mapper.Content), string(newContent)) return source.ToProtocolEdits(pm.Mapper, diff) } @@ -404,7 +431,7 @@ func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, // create output buf := &bytes.Buffer{} ew := progress.NewEventWriter(ctx, "test") - out := io.MultiWriter(ew, progress.NewWorkDoneWriter(work), buf) + out := io.MultiWriter(ew, progress.NewWorkDoneWriter(ctx, work), buf) // Run `go test -run Func` on each test. var failedTests int @@ -487,7 +514,7 @@ func (c *commandHandler) Generate(ctx context.Context, args command.GenerateArgs Args: []string{"-x", pattern}, WorkingDir: args.Dir.SpanURI().Filename(), } - stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(deps.work)) + stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work)) if err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil { return err } @@ -544,9 +571,15 @@ func (s *Server) runGoModUpdateCommands(ctx context.Context, snapshot source.Sna if len(changes) == 0 { return nil } + var documentChanges []protocol.DocumentChanges + for _, change := range changes { + documentChanges = append(documentChanges, protocol.DocumentChanges{ + TextDocumentEdit: &change, + }) + } response, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ Edit: protocol.WorkspaceEdit{ - DocumentChanges: changes, + DocumentChanges: documentChanges, }, }) if err != nil { @@ -580,10 +613,7 @@ func applyFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, } m := protocol.NewColumnMapper(fh.URI(), oldContent) - diff, err := snapshot.View().Options().ComputeEdits(uri, string(oldContent), string(newContent)) - if err != nil { - return nil, err - } + diff := snapshot.View().Options().ComputeEdits(string(oldContent), string(newContent)) edits, err := source.ToProtocolEdits(m, diff) if err != nil { return nil, err @@ -691,7 +721,7 @@ func (c *commandHandler) GenerateGoplsMod(ctx context.Context, args command.URIA if err != nil { return fmt.Errorf("formatting mod file: %w", err) } - filename := filepath.Join(snapshot.View().Folder().Filename(), "gopls.mod") + filename := filepath.Join(v.Folder().Filename(), "gopls.mod") if err := ioutil.WriteFile(filename, content, 0644); err != nil { return fmt.Errorf("writing mod file: %w", err) } @@ -790,34 +820,95 @@ func (c *commandHandler) StartDebugging(ctx context.Context, args command.Debugg return result, nil } -func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) (result command.VulncheckResult, _ error) { +// Copy of pkgLoadConfig defined in internal/lsp/cmd/vulncheck.go +// TODO(hyangah): decide where to define this. +type pkgLoadConfig struct { + // BuildFlags is a list of command-line flags to be passed through to + // the build system's query tool. + BuildFlags []string + + // If Tests is set, the loader includes related test packages. + Tests bool +} + +func (c *commandHandler) RunVulncheckExp(ctx context.Context, args command.VulncheckArgs) error { + if args.URI == "" { + return errors.New("VulncheckArgs is missing URI field") + } err := c.run(ctx, commandConfig{ - progress: "Running vulncheck", + async: true, // need to be async to be cancellable + progress: "govulncheck", requireSave: true, - forURI: args.Dir, // Will dir work? + forURI: args.URI, }, func(ctx context.Context, deps commandDeps) error { view := deps.snapshot.View() opts := view.Options() - if opts == nil || opts.Hooks.Govulncheck == nil { + // quickly test if gopls is compiled to support govulncheck + // by checking vulncheck.Govulncheck. Alternatively, we can continue and + // let the `gopls vulncheck` command fail. This is lighter-weight. + if vulncheck.Govulncheck == nil { return errors.New("vulncheck feature is not available") } - buildFlags := opts.BuildFlags // XXX: is session.Options equivalent to view.Options? + cmd := exec.CommandContext(ctx, os.Args[0], "vulncheck", "-summary", "-config", args.Pattern) + cmd.Dir = filepath.Dir(args.URI.SpanURI().Filename()) + var viewEnv []string if e := opts.EnvSlice(); e != nil { viewEnv = append(os.Environ(), e...) } - cfg := &packages.Config{ - Context: ctx, - Tests: true, // TODO(hyangah): add a field in args. - BuildFlags: buildFlags, - Env: viewEnv, - Dir: args.Dir.SpanURI().Filename(), - // TODO(hyangah): configure overlay + cmd.Env = viewEnv + + // stdin: gopls vulncheck expects JSON-encoded configuration from STDIN when -config flag is set. + var stdin bytes.Buffer + cmd.Stdin = &stdin + + if err := json.NewEncoder(&stdin).Encode(pkgLoadConfig{ + BuildFlags: opts.BuildFlags, + // TODO(hyangah): add `tests` flag in command.VulncheckArgs + }); err != nil { + return fmt.Errorf("failed to pass package load config: %v", err) } - var err error - result, err = opts.Hooks.Govulncheck(ctx, cfg, args) - return err + + // stderr: stream gopls vulncheck's STDERR as progress reports + er := progress.NewEventWriter(ctx, "vulncheck") + stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work)) + cmd.Stderr = stderr + // TODO: can we stream stdout? + stdout, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to run govulncheck: %v", err) + } + + var summary govulncheck.Summary + if err := json.Unmarshal(stdout, &summary); err != nil { + // TODO: for easy debugging, log the failed stdout somewhere? + return fmt.Errorf("failed to parse govulncheck output: %v", err) + } + + vulns := append(summary.Affecting, summary.NonAffecting...) + deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), vulns) + c.s.diagnoseSnapshot(deps.snapshot, nil, false) + + if len(summary.Affecting) == 0 { + return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ + Type: protocol.Info, + Message: "No vulnerabilities found", + }) + } + set := make(map[string]bool) + for _, v := range summary.Affecting { + set[v.OSV.ID] = true + } + list := make([]string, 0, len(set)) + for k := range set { + list = append(list, k) + } + sort.Strings(list) + return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ + Type: protocol.Warning, + Message: fmt.Sprintf("Found %v", strings.Join(list, ", ")), + }) }) - return result, err + return err } diff --git a/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go similarity index 86% rename from internal/lsp/command/command_gen.go rename to gopls/internal/lsp/command/command_gen.go index 22cfeff5bad..615f9509c8c 100644 --- a/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -15,32 +15,33 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) const ( - AddDependency Command = "add_dependency" - AddImport Command = "add_import" - ApplyFix Command = "apply_fix" - CheckUpgrades Command = "check_upgrades" - EditGoDirective Command = "edit_go_directive" - GCDetails Command = "gc_details" - Generate Command = "generate" - GenerateGoplsMod Command = "generate_gopls_mod" - GoGetPackage Command = "go_get_package" - ListImports Command = "list_imports" - ListKnownPackages Command = "list_known_packages" - RegenerateCgo Command = "regenerate_cgo" - RemoveDependency Command = "remove_dependency" - RunTests Command = "run_tests" - RunVulncheckExp Command = "run_vulncheck_exp" - StartDebugging Command = "start_debugging" - Test Command = "test" - Tidy Command = "tidy" - ToggleGCDetails Command = "toggle_gc_details" - UpdateGoSum Command = "update_go_sum" - UpgradeDependency Command = "upgrade_dependency" - Vendor Command = "vendor" + AddDependency Command = "add_dependency" + AddImport Command = "add_import" + ApplyFix Command = "apply_fix" + CheckUpgrades Command = "check_upgrades" + EditGoDirective Command = "edit_go_directive" + GCDetails Command = "gc_details" + Generate Command = "generate" + GenerateGoplsMod Command = "generate_gopls_mod" + GoGetPackage Command = "go_get_package" + ListImports Command = "list_imports" + ListKnownPackages Command = "list_known_packages" + RegenerateCgo Command = "regenerate_cgo" + RemoveDependency Command = "remove_dependency" + ResetGoModDiagnostics Command = "reset_go_mod_diagnostics" + RunTests Command = "run_tests" + RunVulncheckExp Command = "run_vulncheck_exp" + StartDebugging Command = "start_debugging" + Test Command = "test" + Tidy Command = "tidy" + ToggleGCDetails Command = "toggle_gc_details" + UpdateGoSum Command = "update_go_sum" + UpgradeDependency Command = "upgrade_dependency" + Vendor Command = "vendor" ) var Commands = []Command{ @@ -57,6 +58,7 @@ var Commands = []Command{ ListKnownPackages, RegenerateCgo, RemoveDependency, + ResetGoModDiagnostics, RunTests, RunVulncheckExp, StartDebugging, @@ -148,6 +150,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.RemoveDependency(ctx, a0) + case "gopls.reset_go_mod_diagnostics": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return nil, s.ResetGoModDiagnostics(ctx, a0) case "gopls.run_tests": var a0 RunTestsArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -159,7 +167,7 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte if err := UnmarshalArgs(params.Arguments, &a0); err != nil { return nil, err } - return s.RunVulncheckExp(ctx, a0) + return nil, s.RunVulncheckExp(ctx, a0) case "gopls.start_debugging": var a0 DebuggingArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -364,6 +372,18 @@ func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol }, nil } +func NewResetGoModDiagnosticsCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.reset_go_mod_diagnostics", + Arguments: args, + }, nil +} + func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { diff --git a/internal/lsp/command/commandmeta/meta.go b/gopls/internal/lsp/command/commandmeta/meta.go similarity index 98% rename from internal/lsp/command/commandmeta/meta.go rename to gopls/internal/lsp/command/commandmeta/meta.go index a3a357df4b0..11d9940f6d4 100644 --- a/internal/lsp/command/commandmeta/meta.go +++ b/gopls/internal/lsp/command/commandmeta/meta.go @@ -17,7 +17,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/command" ) type Command struct { @@ -52,7 +52,7 @@ func Load() (*packages.Package, []*Command, error) { Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, BuildFlags: []string{"-tags=generate"}, }, - "golang.org/x/tools/internal/lsp/command", + "golang.org/x/tools/gopls/internal/lsp/command", ) if err != nil { return nil, nil, fmt.Errorf("packages.Load: %v", err) diff --git a/internal/lsp/command/gen/gen.go b/gopls/internal/lsp/command/gen/gen.go similarity index 94% rename from internal/lsp/command/gen/gen.go rename to gopls/internal/lsp/command/gen/gen.go index 8f7a2d50313..29428699ee6 100644 --- a/internal/lsp/command/gen/gen.go +++ b/gopls/internal/lsp/command/gen/gen.go @@ -13,7 +13,7 @@ import ( "text/template" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/command/commandmeta" + "golang.org/x/tools/gopls/internal/lsp/command/commandmeta" ) const src = `// Copyright 2021 The Go Authors. All rights reserved. @@ -109,10 +109,10 @@ func Generate() ([]byte, error) { Imports: map[string]bool{ "context": true, "fmt": true, - "golang.org/x/tools/internal/lsp/protocol": true, + "golang.org/x/tools/gopls/internal/lsp/protocol": true, }, } - const thispkg = "golang.org/x/tools/internal/lsp/command" + const thispkg = "golang.org/x/tools/gopls/internal/lsp/command" for _, c := range d.Commands { for _, arg := range c.Args { pth := pkgPath(arg.Type) diff --git a/internal/lsp/command/generate.go b/gopls/internal/lsp/command/generate.go similarity index 88% rename from internal/lsp/command/generate.go rename to gopls/internal/lsp/command/generate.go index 14628c733b5..79ff49b0e33 100644 --- a/internal/lsp/command/generate.go +++ b/gopls/internal/lsp/command/generate.go @@ -12,7 +12,7 @@ import ( "io/ioutil" "os" - "golang.org/x/tools/internal/lsp/command/gen" + "golang.org/x/tools/gopls/internal/lsp/command/gen" ) func main() { diff --git a/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go similarity index 94% rename from internal/lsp/command/interface.go rename to gopls/internal/lsp/command/interface.go index 8e4b1056d32..23b9f655046 100644 --- a/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -17,7 +17,7 @@ package command import ( "context" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // Interface defines the interface gopls exposes for the @@ -98,6 +98,11 @@ type Interface interface { // Removes a dependency from the go.mod file of a module. RemoveDependency(context.Context, RemoveDependencyArgs) error + // ResetGoModDiagnostics: Reset go.mod diagnostics + // + // Reset diagnostics in the go.mod file of a module. + ResetGoModDiagnostics(context.Context, URIArg) error + // GoGetPackage: go get a package // // Runs `go get` to fetch a package. @@ -147,7 +152,7 @@ type Interface interface { // RunVulncheckExp: Run vulncheck (experimental) // // Run vulnerability check (`govulncheck`). - RunVulncheckExp(context.Context, VulncheckArgs) (VulncheckResult, error) + RunVulncheckExp(context.Context, VulncheckArgs) error } type RunTestsArgs struct { @@ -267,21 +272,6 @@ type PackageImport struct { Path string } -type WorkspaceMetadataArgs struct { -} - -type WorkspaceMetadataResult struct { - // All workspaces for this session. - Workspaces []Workspace -} - -type Workspace struct { - // The workspace name. - Name string - // The workspace module directory. - ModuleDir string -} - type DebuggingArgs struct { // Optional: the address (including port) for the debug server to listen on. // If not provided, the debug server will bind to "localhost:0", and the @@ -314,14 +304,13 @@ type DebuggingResult struct { } type VulncheckArgs struct { - // Dir is the directory from which vulncheck will run from. - Dir protocol.DocumentURI + // Any document in the directory from which govulncheck will run. + URI protocol.DocumentURI // Package pattern. E.g. "", ".", "./...". Pattern string - // TODO: Flag []string (flags accepted by govulncheck, e.g., -tests) - // TODO: Format string (json, text) + // TODO: -tests } type VulncheckResult struct { @@ -347,6 +336,7 @@ type StackEntry struct { } // Vuln models an osv.Entry and representative call stacks. +// TODO: deprecate type Vuln struct { // ID is the vulnerability ID (osv.Entry.ID). // https://ossf.github.io/osv-schema/#id-modified-fields @@ -359,8 +349,10 @@ type Vuln struct { Aliases []string `json:",omitempty"` // Symbol is the name of the detected vulnerable function or method. + // Can be empty if the vulnerability exists in required modules, but no vulnerable symbols are used. Symbol string `json:",omitempty"` // PkgPath is the package path of the detected Symbol. + // Can be empty if the vulnerability exists in required modules, but no vulnerable packages are used. PkgPath string `json:",omitempty"` // ModPath is the module path corresponding to PkgPath. // TODO: how do we specify standard library's vulnerability? diff --git a/internal/lsp/command/interface_test.go b/gopls/internal/lsp/command/interface_test.go similarity index 92% rename from internal/lsp/command/interface_test.go rename to gopls/internal/lsp/command/interface_test.go index 9ea30b4463e..de3ce62737f 100644 --- a/internal/lsp/command/interface_test.go +++ b/gopls/internal/lsp/command/interface_test.go @@ -9,7 +9,7 @@ import ( "io/ioutil" "testing" - "golang.org/x/tools/internal/lsp/command/gen" + "golang.org/x/tools/gopls/internal/lsp/command/gen" "golang.org/x/tools/internal/testenv" ) diff --git a/internal/lsp/command/util.go b/gopls/internal/lsp/command/util.go similarity index 100% rename from internal/lsp/command/util.go rename to gopls/internal/lsp/command/util.go diff --git a/internal/lsp/completion.go b/gopls/internal/lsp/completion.go similarity index 92% rename from internal/lsp/completion.go rename to gopls/internal/lsp/completion.go index 06af1bdaec0..465526668a6 100644 --- a/internal/lsp/completion.go +++ b/gopls/internal/lsp/completion.go @@ -10,13 +10,13 @@ import ( "strings" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/source/completion" - "golang.org/x/tools/internal/lsp/template" - "golang.org/x/tools/internal/lsp/work" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/completion" + "golang.org/x/tools/gopls/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/work" ) func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { diff --git a/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go similarity index 96% rename from internal/lsp/completion_test.go rename to gopls/internal/lsp/completion_test.go index d496a40a5cc..23d69ed1269 100644 --- a/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -8,10 +8,10 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" ) func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { diff --git a/internal/lsp/debounce.go b/gopls/internal/lsp/debounce.go similarity index 100% rename from internal/lsp/debounce.go rename to gopls/internal/lsp/debounce.go diff --git a/internal/lsp/debounce_test.go b/gopls/internal/lsp/debounce_test.go similarity index 100% rename from internal/lsp/debounce_test.go rename to gopls/internal/lsp/debounce_test.go diff --git a/internal/lsp/debug/buildinfo_go1.12.go b/gopls/internal/lsp/debug/buildinfo_go1.12.go similarity index 100% rename from internal/lsp/debug/buildinfo_go1.12.go rename to gopls/internal/lsp/debug/buildinfo_go1.12.go diff --git a/internal/lsp/debug/buildinfo_go1.18.go b/gopls/internal/lsp/debug/buildinfo_go1.18.go similarity index 100% rename from internal/lsp/debug/buildinfo_go1.18.go rename to gopls/internal/lsp/debug/buildinfo_go1.18.go diff --git a/internal/lsp/debug/info.go b/gopls/internal/lsp/debug/info.go similarity index 95% rename from internal/lsp/debug/info.go rename to gopls/internal/lsp/debug/info.go index bcc2f4f0605..00752e6f9a3 100644 --- a/internal/lsp/debug/info.go +++ b/gopls/internal/lsp/debug/info.go @@ -16,7 +16,7 @@ import ( "sort" "strings" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source" ) type PrintMode int @@ -38,17 +38,6 @@ type ServerVersion struct { Version string } -type Module struct { - ModuleVersion - Replace *ModuleVersion `json:"replace,omitempty"` -} - -type ModuleVersion struct { - Path string `json:"path,omitempty"` - Version string `json:"version,omitempty"` - Sum string `json:"sum,omitempty"` -} - // VersionInfo returns the build info for the gopls process. If it was not // built in module mode, we return a GOPATH-specific message with the // hardcoded version. diff --git a/internal/lsp/debug/info_test.go b/gopls/internal/lsp/debug/info_test.go similarity index 100% rename from internal/lsp/debug/info_test.go rename to gopls/internal/lsp/debug/info_test.go diff --git a/internal/lsp/debug/log/log.go b/gopls/internal/lsp/debug/log/log.go similarity index 95% rename from internal/lsp/debug/log/log.go rename to gopls/internal/lsp/debug/log/log.go index 44638f8a582..e3eaa106f7e 100644 --- a/internal/lsp/debug/log/log.go +++ b/gopls/internal/lsp/debug/log/log.go @@ -12,7 +12,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) // Level parameterizes log severity. diff --git a/internal/lsp/debug/metrics.go b/gopls/internal/lsp/debug/metrics.go similarity index 97% rename from internal/lsp/debug/metrics.go rename to gopls/internal/lsp/debug/metrics.go index 8efc1d495e0..c8da803d6b1 100644 --- a/internal/lsp/debug/metrics.go +++ b/gopls/internal/lsp/debug/metrics.go @@ -7,7 +7,7 @@ package debug import ( "golang.org/x/tools/internal/event/export/metric" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) var ( diff --git a/internal/lsp/debug/rpc.go b/gopls/internal/lsp/debug/rpc.go similarity index 99% rename from internal/lsp/debug/rpc.go rename to gopls/internal/lsp/debug/rpc.go index 033ee3797fb..5610021479c 100644 --- a/internal/lsp/debug/rpc.go +++ b/gopls/internal/lsp/debug/rpc.go @@ -17,7 +17,7 @@ import ( "golang.org/x/tools/internal/event/core" "golang.org/x/tools/internal/event/export" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) var RPCTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` diff --git a/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go similarity index 98% rename from internal/lsp/debug/serve.go rename to gopls/internal/lsp/debug/serve.go index 0bdee92c5e0..248041ed68a 100644 --- a/internal/lsp/debug/serve.go +++ b/gopls/internal/lsp/debug/serve.go @@ -26,6 +26,10 @@ import ( "sync" "time" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug/log" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/core" "golang.org/x/tools/internal/event/export" @@ -34,12 +38,7 @@ import ( "golang.org/x/tools/internal/event/export/prometheus" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug/log" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" ) type contextKeyType int @@ -88,10 +87,7 @@ func (st *State) Caches() []*cache.Cache { var caches []*cache.Cache seen := make(map[string]struct{}) for _, client := range st.Clients() { - cache, ok := client.Session.Cache().(*cache.Cache) - if !ok { - continue - } + cache := client.Session.Cache() if _, found := seen[cache.ID()]; found { continue } @@ -208,7 +204,7 @@ func (st *State) addClient(session *cache.Session) { } // DropClient removes a client from the set being served. -func (st *State) dropClient(session source.Session) { +func (st *State) dropClient(session *cache.Session) { st.mu.Lock() defer st.mu.Unlock() for i, c := range st.clients { @@ -320,7 +316,8 @@ func (i *Instance) getFile(r *http.Request) interface{} { return nil } for _, o := range s.Overlays() { - if o.FileIdentity().Hash == identifier { + // TODO(adonovan): understand and document this comparison. + if o.FileIdentity().Hash.String() == identifier { return o } } diff --git a/internal/lsp/debug/trace.go b/gopls/internal/lsp/debug/trace.go similarity index 93% rename from internal/lsp/debug/trace.go rename to gopls/internal/lsp/debug/trace.go index ca612867a5d..bb402cfaa8f 100644 --- a/internal/lsp/debug/trace.go +++ b/gopls/internal/lsp/debug/trace.go @@ -119,8 +119,6 @@ func formatEvent(ctx context.Context, ev core.Event, lm label.Map) string { } func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { - t.mu.Lock() - defer t.mu.Unlock() span := export.GetSpan(ctx) if span == nil { return ctx @@ -128,11 +126,8 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) switch { case event.IsStart(ev): - if t.sets == nil { - t.sets = make(map[string]*traceSet) - t.unfinished = make(map[export.SpanContext]*traceData) - } - // just starting, add it to the unfinished map + // Just starting: add it to the unfinished map. + // Allocate before the critical section. td := &traceData{ TraceID: span.ID.TraceID, SpanID: span.ID.SpanID, @@ -141,6 +136,13 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) Start: span.Start().At(), Tags: renderLabels(span.Start()), } + + t.mu.Lock() + defer t.mu.Unlock() + if t.sets == nil { + t.sets = make(map[string]*traceSet) + t.unfinished = make(map[export.SpanContext]*traceData) + } t.unfinished[span.ID] = td // and wire up parents if we have them if !span.ParentID.IsValid() { @@ -155,7 +157,19 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) parent.Children = append(parent.Children, td) case event.IsEnd(ev): - // finishing, must be already in the map + // Finishing: must be already in the map. + // Allocate events before the critical section. + events := span.Events() + tdEvents := make([]traceEvent, len(events)) + for i, event := range events { + tdEvents[i] = traceEvent{ + Time: event.At(), + Tags: renderLabels(event), + } + } + + t.mu.Lock() + defer t.mu.Unlock() td, found := t.unfinished[span.ID] if !found { return ctx // if this happens we are in a bad place @@ -164,14 +178,7 @@ func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) td.Finish = span.Finish().At() td.Duration = span.Finish().At().Sub(span.Start().At()) - events := span.Events() - td.Events = make([]traceEvent, len(events)) - for i, event := range events { - td.Events[i] = traceEvent{ - Time: event.At(), - Tags: renderLabels(event), - } - } + td.Events = tdEvents set, ok := t.sets[span.Name] if !ok { diff --git a/internal/lsp/definition.go b/gopls/internal/lsp/definition.go similarity index 92% rename from internal/lsp/definition.go rename to gopls/internal/lsp/definition.go index 9487c684327..d2ad4742b97 100644 --- a/internal/lsp/definition.go +++ b/gopls/internal/lsp/definition.go @@ -8,9 +8,9 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" ) func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) { diff --git a/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go similarity index 78% rename from internal/lsp/diagnostics.go rename to gopls/internal/lsp/diagnostics.go index 0837b22cc22..a01898b26a4 100644 --- a/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -15,15 +15,15 @@ import ( "sync" "time" + "golang.org/x/tools/gopls/internal/lsp/debug/log" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/work" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/log" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" - "golang.org/x/tools/internal/lsp/work" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/xcontext" ) @@ -37,21 +37,48 @@ const ( typeCheckSource orphanedSource workSource + modCheckUpgradesSource + modVulncheckSource ) // A diagnosticReport holds results for a single diagnostic source. type diagnosticReport struct { - snapshotID uint64 - publishedHash string + snapshotID uint64 // snapshot ID on which the report was computed + publishedHash string // last published hash for this (URI, source) diags map[string]*source.Diagnostic } // fileReports holds a collection of diagnostic reports for a single file, as // well as the hash of the last published set of diagnostics. type fileReports struct { - snapshotID uint64 + // publishedSnapshotID is the last snapshot ID for which we have "published" + // diagnostics (though the publishDiagnostics notification may not have + // actually been sent, if nothing changed). + // + // Specifically, publishedSnapshotID is updated to a later snapshot ID when + // we either: + // (1) publish diagnostics for the file for a snapshot, or + // (2) determine that published diagnostics are valid for a new snapshot. + // + // Notably publishedSnapshotID may not match the snapshot id on individual reports in + // the reports map: + // - we may have published partial diagnostics from only a subset of + // diagnostic sources for which new results have been computed, or + // - we may have started computing reports for an even new snapshot, but not + // yet published. + // + // This prevents gopls from publishing stale diagnostics. + publishedSnapshotID uint64 + + // publishedHash is a hash of the latest diagnostics published for the file. publishedHash string - reports map[diagnosticSource]diagnosticReport + + // If set, mustPublish marks diagnostics as needing publication, independent + // of whether their publishedHash has changed. + mustPublish bool + + // The last stored diagnostics for each diagnostic source. + reports map[diagnosticSource]diagnosticReport } func (d diagnosticSource) String() string { @@ -66,6 +93,12 @@ func (d diagnosticSource) String() string { return "FromTypeChecking" case orphanedSource: return "FromOrphans" + case workSource: + return "FromGoWork" + case modCheckUpgradesSource: + return "FromCheckForUpgrades" + case modVulncheckSource: + return "FromModVulncheck" default: return fmt.Sprintf("From?%d?", d) } @@ -186,6 +219,10 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn defer done() // Wait for a free diagnostics slot. + // TODO(adonovan): opt: shouldn't it be the analysis implementation's + // job to de-dup and limit resource consumption? In any case this + // this function spends most its time waiting for awaitLoaded, at + // least initially. select { case <-ctx.Done(): return @@ -195,43 +232,62 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn <-s.diagnosticsSema }() - // First, diagnose the go.mod file. - modReports, modErr := mod.Diagnostics(ctx, snapshot) + // common code for dispatching diagnostics + store := func(dsource diagnosticSource, operation string, diagsByFileID map[source.VersionedFileIdentity][]*source.Diagnostic, err error) { + if err != nil { + event.Error(ctx, "warning: while "+operation, err, + tag.Directory.Of(snapshot.View().Folder().Filename()), + tag.Snapshot.Of(snapshot.ID())) + } + for id, diags := range diagsByFileID { + if id.URI == "" { + event.Error(ctx, "missing URI while "+operation, fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) + continue + } + s.storeDiagnostics(snapshot, id.URI, dsource, diags) + } + } + + // Diagnose go.mod upgrades. + upgradeReports, upgradeErr := mod.UpgradeDiagnostics(ctx, snapshot) if ctx.Err() != nil { log.Trace.Log(ctx, "diagnose cancelled") return } - if modErr != nil { - event.Error(ctx, "warning: diagnose go.mod", modErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) - } - for id, diags := range modReports { - if id.URI == "" { - event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) - continue - } - s.storeDiagnostics(snapshot, id.URI, modSource, diags) + store(modCheckUpgradesSource, "diagnosing go.mod upgrades", upgradeReports, upgradeErr) + + // Diagnose vulnerabilities. + vulnReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) + if ctx.Err() != nil { + log.Trace.Log(ctx, "diagnose cancelled") + return } + store(modVulncheckSource, "diagnosing vulnerabilities", vulnReports, vulnErr) - // Diagnose the go.work file, if it exists. + // Diagnose go.work file. workReports, workErr := work.Diagnostics(ctx, snapshot) if ctx.Err() != nil { log.Trace.Log(ctx, "diagnose cancelled") return } - if workErr != nil { - event.Error(ctx, "warning: diagnose go.work", workErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) - } - for id, diags := range workReports { - if id.URI == "" { - event.Error(ctx, "missing URI for work file diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) - continue - } - s.storeDiagnostics(snapshot, id.URI, workSource, diags) + store(workSource, "diagnosing go.work file", workReports, workErr) + + // All subsequent steps depend on the completion of + // type-checking of the all active packages in the workspace. + // This step may take many seconds initially. + // (mod.Diagnostics would implicitly wait for this too, + // but the control is clearer if it is explicit here.) + activePkgs, activeErr := snapshot.ActivePackages(ctx) + + // Diagnose go.mod file. + modReports, modErr := mod.Diagnostics(ctx, snapshot) + if ctx.Err() != nil { + log.Trace.Log(ctx, "diagnose cancelled") + return } + store(modSource, "diagnosing go.mod file", modReports, modErr) - // Diagnose all of the packages in the workspace. - wsPkgs, err := snapshot.ActivePackages(ctx) - if s.shouldIgnoreError(ctx, snapshot, err) { + if s.shouldIgnoreError(ctx, snapshot, activeErr) { return } criticalErr := snapshot.GetCriticalError(ctx) @@ -242,7 +298,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn // error progress reports will be closed. s.showCriticalErrorStatus(ctx, snapshot, criticalErr) - // There may be .tmpl files. + // Diagnose template (.tmpl) files. for _, f := range snapshot.Templates() { diags := template.Diagnose(f) s.storeDiagnostics(snapshot, f.URI(), typeCheckSource, diags) @@ -250,29 +306,31 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn // If there are no workspace packages, there is nothing to diagnose and // there are no orphaned files. - if len(wsPkgs) == 0 { + if len(activePkgs) == 0 { return } + // Run go/analysis diagnosis of packages in parallel. + // TODO(adonovan): opt: it may be more efficient to + // have diagnosePkg take a set of packages. var ( wg sync.WaitGroup seen = map[span.URI]struct{}{} ) - for _, pkg := range wsPkgs { - wg.Add(1) - + for _, pkg := range activePkgs { for _, pgf := range pkg.CompiledGoFiles() { seen[pgf.URI] = struct{}{} } + wg.Add(1) go func(pkg source.Package) { defer wg.Done() - s.diagnosePkg(ctx, snapshot, pkg, forceAnalysis) }(pkg) } wg.Wait() + // Orphaned files. // Confirm that every opened file belongs to a package (if any exist in // the workspace). Otherwise, add a diagnostic to the file. for _, o := range s.session.Overlays() { @@ -356,6 +414,24 @@ func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg } } +// mustPublishDiagnostics marks the uri as needing publication, independent of +// whether the published contents have changed. +// +// This can be used for ensuring gopls publishes diagnostics after certain file +// events. +func (s *Server) mustPublishDiagnostics(uri span.URI) { + s.diagnosticsMu.Lock() + defer s.diagnosticsMu.Unlock() + + if s.diagnostics[uri] == nil { + s.diagnostics[uri] = &fileReports{ + publishedHash: hashDiagnostics(), // Hash for 0 diagnostics. + reports: map[diagnosticSource]diagnosticReport{}, + } + } + s.diagnostics[uri].mustPublish = true +} + // storeDiagnostics stores results from a single diagnostic source. If merge is // true, it merges results into any existing results for this snapshot. func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsource diagnosticSource, diags []*source.Diagnostic) { @@ -365,6 +441,7 @@ func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsourc if fh == nil { return } + s.diagnosticsMu.Lock() defer s.diagnosticsMu.Unlock() if s.diagnostics[uri] == nil { @@ -412,7 +489,7 @@ func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Sn var errMsg string if err != nil { event.Error(ctx, "errors loading workspace", err.MainError, tag.Snapshot.Of(snapshot.ID()), tag.Directory.Of(snapshot.View().Folder())) - for _, d := range err.DiagList { + for _, d := range err.Diagnostics { s.storeDiagnostics(snapshot, d.URI, modSource, []*source.Diagnostic{d}) } errMsg = strings.ReplaceAll(err.MainError.Error(), "\n", " ") @@ -454,8 +531,8 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if snapshot.IsBuiltin(ctx, fh.URI()) { return nil } - pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace, false) - if len(pkgs) > 0 || err == nil { + pkgs, _ := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace, false) + if len(pkgs) > 0 { return nil } pgf, err := snapshot.ParseGo(ctx, fh, source.ParseHeader) @@ -465,11 +542,7 @@ func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snaps if !pgf.File.Name.Pos().IsValid() { return nil } - spn, err := span.NewRange(snapshot.FileSet(), pgf.File.Name.Pos(), pgf.File.Name.End()).Span() - if err != nil { - return nil - } - rng, err := pgf.Mapper.Range(spn) + rng, err := pgf.Mapper.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) if err != nil { return nil } @@ -499,6 +572,7 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so s.diagnosticsMu.Lock() defer s.diagnosticsMu.Unlock() + // TODO(rfindley): remove this noisy (and not useful) logging. published := 0 defer func() { log.Trace.Logf(ctx, "published %d diagnostics", published) @@ -510,7 +584,7 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so // If we've already delivered diagnostics for a future snapshot for this // file, do not deliver them. - if r.snapshotID > snapshot.ID() { + if r.publishedSnapshotID > snapshot.ID() { continue } anyReportsChanged := false @@ -539,10 +613,10 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so } source.SortDiagnostics(diags) hash := hashDiagnostics(diags...) - if hash == r.publishedHash { + if hash == r.publishedHash && !r.mustPublish { // Update snapshotID to be the latest snapshot for which this diagnostic // hash is valid. - r.snapshotID = snapshot.ID() + r.publishedSnapshotID = snapshot.ID() continue } var version int32 @@ -556,7 +630,8 @@ func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot so }); err == nil { published++ r.publishedHash = hash - r.snapshotID = snapshot.ID() + r.mustPublish = false // diagnostics have been successfully published + r.publishedSnapshotID = snapshot.ID() for dsource, hash := range reportHashes { report := r.reports[dsource] report.publishedHash = hash diff --git a/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go similarity index 91% rename from internal/lsp/fake/client.go rename to gopls/internal/lsp/fake/client.go index fdc67a6cc64..f44bd73b215 100644 --- a/internal/lsp/fake/client.go +++ b/gopls/internal/lsp/fake/client.go @@ -8,7 +8,7 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // ClientHooks are called to handle the corresponding client LSP method. @@ -30,6 +30,10 @@ type Client struct { hooks ClientHooks } +func (c *Client) CodeLensRefresh(context.Context) error { return nil } + +func (c *Client) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } + func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error { if c.hooks.OnShowMessage != nil { return c.hooks.OnShowMessage(ctx, params) @@ -74,10 +78,11 @@ func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { results := make([]interface{}, len(p.Items)) for i, item := range p.Items { - if item.Section != "gopls" { - continue + if item.Section == "gopls" { + c.editor.mu.Lock() + results[i] = c.editor.settingsLocked() + c.editor.mu.Unlock() } - results[i] = c.editor.configuration() } return results, nil } @@ -120,7 +125,7 @@ func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceE return &protocol.ApplyWorkspaceEditResult{FailureReason: "Edit.Changes is unsupported"}, nil } for _, change := range params.Edit.DocumentChanges { - if err := c.editor.applyProtocolEdit(ctx, change); err != nil { + if err := c.editor.applyDocumentChange(ctx, change); err != nil { return nil, err } } diff --git a/internal/lsp/fake/doc.go b/gopls/internal/lsp/fake/doc.go similarity index 100% rename from internal/lsp/fake/doc.go rename to gopls/internal/lsp/fake/doc.go diff --git a/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go similarity index 58% rename from internal/lsp/fake/edit.go rename to gopls/internal/lsp/fake/edit.go index 8b04c390fc5..3eb13ea2f4c 100644 --- a/internal/lsp/fake/edit.go +++ b/gopls/internal/lsp/fake/edit.go @@ -6,14 +6,16 @@ package fake import ( "fmt" - "sort" "strings" + "unicode/utf8" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/diff" ) -// Pos represents a position in a text buffer. Both Line and Column are -// 0-indexed. +// Pos represents a position in a text buffer. +// Both Line and Column are 0-indexed. +// Column counts runes. type Pos struct { Line, Column int } @@ -105,53 +107,51 @@ func inText(p Pos, content []string) bool { return true } -// editContent implements a simplistic, inefficient algorithm for applying text -// edits to our buffer representation. It returns an error if the edit is -// invalid for the current content. -func editContent(content []string, edits []Edit) ([]string, error) { - newEdits := make([]Edit, len(edits)) - copy(newEdits, edits) - sort.Slice(newEdits, func(i, j int) bool { - if newEdits[i].Start.Line < newEdits[j].Start.Line { - return true - } - if newEdits[i].Start.Line > newEdits[j].Start.Line { - return false - } - return newEdits[i].Start.Column < newEdits[j].Start.Column - }) - - // Validate edits. - for _, edit := range newEdits { - if edit.End.Line < edit.Start.Line || (edit.End.Line == edit.Start.Line && edit.End.Column < edit.Start.Column) { - return nil, fmt.Errorf("invalid edit: end %v before start %v", edit.End, edit.Start) - } - if !inText(edit.Start, content) { - return nil, fmt.Errorf("start position %v is out of bounds", edit.Start) - } - if !inText(edit.End, content) { - return nil, fmt.Errorf("end position %v is out of bounds", edit.End) +// applyEdits applies the edits to a file with the specified lines, +// and returns a new slice containing the lines of the patched file. +// It is a wrapper around diff.Apply; see that function for preconditions. +func applyEdits(lines []string, edits []Edit) ([]string, error) { + src := strings.Join(lines, "\n") + + // Build a table of byte offset of start of each line. + lineOffset := make([]int, len(lines)+1) + offset := 0 + for i, line := range lines { + lineOffset[i] = offset + offset += len(line) + len("\n") + } + lineOffset[len(lines)] = offset // EOF + + var badCol error + posToOffset := func(pos Pos) int { + offset := lineOffset[pos.Line] + // Convert pos.Column (runes) to a UTF-8 byte offset. + if pos.Line < len(lines) { + for i := 0; i < pos.Column; i++ { + r, sz := utf8.DecodeRuneInString(src[offset:]) + if r == '\n' && badCol == nil { + badCol = fmt.Errorf("bad column") + } + offset += sz + } } + return offset } - var ( - b strings.Builder - line, column int - ) - advance := func(toLine, toColumn int) { - for ; line < toLine; line++ { - b.WriteString(string([]rune(content[line])[column:]) + "\n") - column = 0 + // Convert fake.Edits to diff.Edits + diffEdits := make([]diff.Edit, len(edits)) + for i, edit := range edits { + diffEdits[i] = diff.Edit{ + Start: posToOffset(edit.Start), + End: posToOffset(edit.End), + New: edit.Text, } - b.WriteString(string([]rune(content[line])[column:toColumn])) - column = toColumn } - for _, edit := range newEdits { - advance(edit.Start.Line, edit.Start.Column) - b.WriteString(edit.Text) - line = edit.End.Line - column = edit.End.Column + + patched, err := diff.Apply(src, diffEdits) + if err != nil { + return nil, err } - advance(len(content)-1, len([]rune(content[len(content)-1]))) - return strings.Split(b.String(), "\n"), nil + + return strings.Split(patched, "\n"), badCol } diff --git a/internal/lsp/fake/edit_test.go b/gopls/internal/lsp/fake/edit_test.go similarity index 95% rename from internal/lsp/fake/edit_test.go rename to gopls/internal/lsp/fake/edit_test.go index 4fa23bdb74a..f87d9210336 100644 --- a/internal/lsp/fake/edit_test.go +++ b/gopls/internal/lsp/fake/edit_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -func TestApplyEdit(t *testing.T) { +func TestApplyEdits(t *testing.T) { tests := []struct { label string content string @@ -82,7 +82,7 @@ func TestApplyEdit(t *testing.T) { test := test t.Run(test.label, func(t *testing.T) { lines := strings.Split(test.content, "\n") - newLines, err := editContent(lines, test.edits) + newLines, err := applyEdits(lines, test.edits) if (err != nil) != test.wantErr { t.Errorf("got err %v, want error: %t", err, test.wantErr) } diff --git a/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go similarity index 72% rename from internal/lsp/fake/editor.go rename to gopls/internal/lsp/fake/editor.go index 06b90bb84e5..f73301d674c 100644 --- a/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -16,32 +16,32 @@ import ( "strings" "sync" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/jsonrpc2/servertest" + "golang.org/x/tools/internal/xcontext" ) // Editor is a fake editor client. It keeps track of client state and can be // used for writing LSP tests. type Editor struct { - Config EditorConfig // Server, client, and sandbox are concurrency safe and written only // at construction time, so do not require synchronization. Server protocol.Server + cancelConn func() serverConn jsonrpc2.Conn client *Client sandbox *Sandbox defaultEnv map[string]string - // Since this editor is intended just for testing, we use very coarse - // locking. - mu sync.Mutex - // Editor state. - buffers map[string]buffer - // Capabilities / Options - serverCapabilities protocol.ServerCapabilities + mu sync.Mutex // guards config, buffers, serverCapabilities + config EditorConfig // editor configuration + buffers map[string]buffer // open buffers (relative path -> buffer content) + serverCapabilities protocol.ServerCapabilities // capabilities / options // Call metrics for the purpose of expectations. This is done in an ad-hoc // manner for now. Perhaps in the future we should do something more @@ -51,16 +51,18 @@ type Editor struct { calls CallCounts } +// CallCounts tracks the number of protocol notifications of different types. type CallCounts struct { DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64 } +// buffer holds information about an open buffer in the editor. type buffer struct { - windowsLineEndings bool - version int - path string - lines []string - dirty bool + windowsLineEndings bool // use windows line endings when merging lines + version int // monotonic version; incremented on edits + path string // relative path in the workspace + lines []string // line content + dirty bool // if true, content is unsaved (TODO(rfindley): rename this field) } func (b buffer) text() string { @@ -77,21 +79,11 @@ func (b buffer) text() string { // // The zero value for EditorConfig should correspond to its defaults. type EditorConfig struct { - Env map[string]string - BuildFlags []string - - // CodeLenses is a map defining whether codelens are enabled, keyed by the - // codeLens command. CodeLenses which are not present in this map are left in - // their default state. - CodeLenses map[string]bool - - // SymbolMatcher is the config associated with the "symbolMatcher" gopls - // config option. - SymbolMatcher, SymbolStyle *string - - // LimitWorkspaceScope is true if the user does not want to expand their - // workspace scope to the entire module. - LimitWorkspaceScope bool + // Env holds environment variables to apply on top of the default editor + // environment. When applying these variables, the special string + // $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working + // directory. + Env map[string]string // WorkspaceFolders is the workspace folders to configure on the LSP server, // relative to the sandbox workdir. @@ -101,14 +93,6 @@ type EditorConfig struct { // To explicitly send no workspace folders, use an empty (non-nil) slice. WorkspaceFolders []string - // AllExperiments sets the "allExperiments" configuration, which enables - // all of gopls's opt-in settings. - AllExperiments bool - - // Whether to send the current process ID, for testing data that is joined to - // the PID. This can only be set by one test. - SendPID bool - // Whether to edit files with windows line endings. WindowsLineEndings bool @@ -120,14 +104,8 @@ type EditorConfig struct { // "gotmpl" -> ".*tmpl" FileAssociations map[string]string - // Settings holds arbitrary additional settings to apply to the gopls config. - // TODO(rfindley): replace existing EditorConfig fields with Settings. + // Settings holds user-provided configuration for the LSP server. Settings map[string]interface{} - - ImportShortcut string - DirectoryFilters []string - VerboseOutput bool - ExperimentalUseInvalidMetadata bool } // NewEditor Creates a new Editor. @@ -136,7 +114,7 @@ func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { buffers: make(map[string]buffer), sandbox: sandbox, defaultEnv: sandbox.GoEnv(), - Config: config, + config: config, } } @@ -146,16 +124,21 @@ func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { // // It returns the editor, so that it may be called as follows: // -// editor, err := NewEditor(s).Connect(ctx, conn) -func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) { +// editor, err := NewEditor(s).Connect(ctx, conn, hooks) +func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks) (*Editor, error) { + bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx)) + conn := connector.Connect(bgCtx) + e.cancelConn = cancelConn + e.serverConn = conn e.Server = protocol.ServerDispatcher(conn) e.client = &Client{editor: e, hooks: hooks} - conn.Go(ctx, + conn.Go(bgCtx, protocol.Handlers( protocol.ClientHandler(e.client, jsonrpc2.MethodNotFound))) - if err := e.initialize(ctx, e.Config.WorkspaceFolders); err != nil { + + if err := e.initialize(ctx); err != nil { return nil, err } e.sandbox.Workdir.AddWatcher(e.onFileChanges) @@ -198,6 +181,10 @@ func (e *Editor) Close(ctx context.Context) error { if err := e.Exit(ctx); err != nil { return err } + defer func() { + e.cancelConn() + }() + // called close on the editor should result in the connection closing select { case <-e.serverConn.Done(): @@ -213,90 +200,61 @@ func (e *Editor) Client() *Client { return e.client } -func (e *Editor) overlayEnv() map[string]string { +// settingsLocked builds the settings map for use in LSP settings RPCs. +// +// e.mu must be held while calling this function. +func (e *Editor) settingsLocked() map[string]interface{} { env := make(map[string]string) for k, v := range e.defaultEnv { - v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename()) env[k] = v } - for k, v := range e.Config.Env { + for k, v := range e.config.Env { + env[k] = v + } + for k, v := range env { v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename()) env[k] = v } - return env -} -func (e *Editor) configuration() map[string]interface{} { - config := map[string]interface{}{ + settings := map[string]interface{}{ + "env": env, + + // Use verbose progress reporting so that regtests can assert on + // asynchronous operations being completed (such as diagnosing a snapshot). "verboseWorkDoneProgress": true, - "env": e.overlayEnv(), - "expandWorkspaceToModule": !e.Config.LimitWorkspaceScope, - "completionBudget": "10s", - } - for k, v := range e.Config.Settings { - config[k] = v - } + // Set a generous completion budget, so that tests don't flake because + // completions are too slow. + "completionBudget": "10s", - if e.Config.BuildFlags != nil { - config["buildFlags"] = e.Config.BuildFlags - } - if e.Config.DirectoryFilters != nil { - config["directoryFilters"] = e.Config.DirectoryFilters - } - if e.Config.ExperimentalUseInvalidMetadata { - config["experimentalUseInvalidMetadata"] = true - } - if e.Config.CodeLenses != nil { - config["codelenses"] = e.Config.CodeLenses - } - if e.Config.SymbolMatcher != nil { - config["symbolMatcher"] = *e.Config.SymbolMatcher - } - if e.Config.SymbolStyle != nil { - config["symbolStyle"] = *e.Config.SymbolStyle - } - if e.Config.AllExperiments { - config["allExperiments"] = true - } - - if e.Config.VerboseOutput { - config["verboseOutput"] = true + // Shorten the diagnostic delay to speed up test execution (else we'd add + // the default delay to each assertion about diagnostics) + "diagnosticsDelay": "10ms", } - if e.Config.ImportShortcut != "" { - config["importShortcut"] = e.Config.ImportShortcut + for k, v := range e.config.Settings { + if k == "env" { + panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead") + } + settings[k] = v } - config["diagnosticsDelay"] = "10ms" - - // ExperimentalWorkspaceModule is only set as a mode, not a configuration. - return config + return settings } -func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error { +func (e *Editor) initialize(ctx context.Context) error { params := &protocol.ParamInitialize{} params.ClientInfo.Name = "fakeclient" params.ClientInfo.Version = "v1.0.0" - - if workspaceFolders == nil { - workspaceFolders = []string{string(e.sandbox.Workdir.RelativeTo)} - } - for _, folder := range workspaceFolders { - params.WorkspaceFolders = append(params.WorkspaceFolders, protocol.WorkspaceFolder{ - URI: string(e.sandbox.Workdir.URI(folder)), - Name: filepath.Base(folder), - }) - } - + e.mu.Lock() + params.WorkspaceFolders = e.makeWorkspaceFoldersLocked() + params.InitializationOptions = e.settingsLocked() + e.mu.Unlock() params.Capabilities.Workspace.Configuration = true params.Capabilities.Window.WorkDoneProgress = true + // TODO: set client capabilities params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated} - params.InitializationOptions = e.configuration() - if e.Config.SendPID { - params.ProcessID = int32(os.Getpid()) - } params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true @@ -307,12 +265,21 @@ func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) erro "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator", } + params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{ + "declaration", "definition", "readonly", "static", + "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", + } // This is a bit of a hack, since the fake editor doesn't actually support // watching changed files that match a specific glob pattern. However, the // editor does send didChangeWatchedFiles notifications, so set this to // true. params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true + params.Capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{ + ResourceOperations: []protocol.ResourceOperationKind{ + "rename", + }, + } params.Trace = "messages" // TODO: support workspace folders. @@ -333,6 +300,27 @@ func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) erro return nil } +// makeWorkspaceFoldersLocked creates a slice of workspace folders to use for +// this editing session, based on the editor configuration. +// +// e.mu must be held while calling this function. +func (e *Editor) makeWorkspaceFoldersLocked() (folders []protocol.WorkspaceFolder) { + paths := e.config.WorkspaceFolders + if len(paths) == 0 { + paths = append(paths, string(e.sandbox.Workdir.RelativeTo)) + } + + for _, path := range paths { + uri := string(e.sandbox.Workdir.URI(path)) + folders = append(folders, protocol.WorkspaceFolder{ + URI: uri, + Name: filepath.Base(uri), + }) + } + + return folders +} + // onFileChanges is registered to be called by the Workdir on any writes that // go through the Workdir API. It is called synchronously by the Workdir. func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { @@ -382,7 +370,12 @@ func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { } // OpenFile creates a buffer for the given workdir-relative file. +// +// If the file is already open, it is a no-op. func (e *Editor) OpenFile(ctx context.Context, path string) error { + if e.HasBuffer(path) { + return nil + } content, err := e.sandbox.Workdir.ReadFile(path) if err != nil { return err @@ -397,24 +390,41 @@ func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { } func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error { + e.mu.Lock() + + if _, ok := e.buffers[path]; ok { + e.mu.Unlock() + return fmt.Errorf("buffer %q already exists", path) + } + buf := buffer{ - windowsLineEndings: e.Config.WindowsLineEndings, + windowsLineEndings: e.config.WindowsLineEndings, version: 1, path: path, lines: lines(content), dirty: dirty, } - e.mu.Lock() - defer e.mu.Unlock() e.buffers[path] = buf - item := protocol.TextDocumentItem{ + item := e.textDocumentItem(buf) + e.mu.Unlock() + + return e.sendDidOpen(ctx, item) +} + +// textDocumentItem builds a protocol.TextDocumentItem for the given buffer. +// +// Precondition: e.mu must be held. +func (e *Editor) textDocumentItem(buf buffer) protocol.TextDocumentItem { + return protocol.TextDocumentItem{ URI: e.sandbox.Workdir.URI(buf.path), - LanguageID: e.languageID(buf.path), + LanguageID: languageID(buf.path, e.config.FileAssociations), Version: int32(buf.version), Text: buf.text(), } +} +func (e *Editor) sendDidOpen(ctx context.Context, item protocol.TextDocumentItem) error { if e.Server != nil { if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ TextDocument: item, @@ -436,9 +446,11 @@ var defaultFileAssociations = map[string]*regexp.Regexp{ "gotmpl": regexp.MustCompile(`^.*tmpl$`), } -func (e *Editor) languageID(p string) string { +// languageID returns the language identifier for the path p given the user +// configured fileAssociations. +func languageID(p string, fileAssociations map[string]string) string { base := path.Base(p) - for lang, re := range e.Config.FileAssociations { + for lang, re := range fileAssociations { re := regexp.MustCompile(re) if re.MatchString(base) { return lang @@ -472,9 +484,13 @@ func (e *Editor) CloseBuffer(ctx context.Context, path string) error { delete(e.buffers, path) e.mu.Unlock() + return e.sendDidClose(ctx, e.TextDocumentIdentifier(path)) +} + +func (e *Editor) sendDidClose(ctx context.Context, doc protocol.TextDocumentIdentifier) error { if e.Server != nil { if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: doc, }); err != nil { return fmt.Errorf("DidClose: %w", err) } @@ -485,7 +501,7 @@ func (e *Editor) CloseBuffer(ctx context.Context, path string) error { return nil } -func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier { +func (e *Editor) TextDocumentIdentifier(path string) protocol.TextDocumentIdentifier { return protocol.TextDocumentIdentifier{ URI: e.sandbox.Workdir.URI(path), } @@ -517,7 +533,7 @@ func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) erro includeText = syncOptions.Save.IncludeText } - docID := e.textDocumentIdentifier(buf.path) + docID := e.TextDocumentIdentifier(buf.path) if e.Server != nil { if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{ TextDocument: docID, @@ -708,11 +724,9 @@ func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit if !ok { return fmt.Errorf("unknown buffer %q", path) } - content := make([]string, len(buf.lines)) - copy(content, buf.lines) - content, err := editContent(content, edits) + content, err := applyEdits(buf.lines, edits) if err != nil { - return err + return fmt.Errorf("editing %q: %v; edits:\n%v", path, err, edits) } return e.setBufferContentLocked(ctx, path, true, content, edits) } @@ -739,7 +753,7 @@ func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty params := &protocol.DidChangeTextDocumentParams{ TextDocument: protocol.VersionedTextDocumentIdentifier{ Version: int32(buf.version), - TextDocumentIdentifier: e.textDocumentIdentifier(buf.path), + TextDocumentIdentifier: e.TextDocumentIdentifier(buf.path), }, ContentChanges: evts, } @@ -864,14 +878,16 @@ func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol // ApplyCodeAction applies the given code action. func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction) error { for _, change := range action.Edit.DocumentChanges { - path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) - if int32(e.buffers[path].version) != change.TextDocument.Version { - // Skip edits for old versions. - continue - } - edits := convertEdits(change.Edits) - if err := e.EditBuffer(ctx, path, edits); err != nil { - return fmt.Errorf("editing buffer %q: %w", path, err) + if change.TextDocumentEdit != nil { + path := e.sandbox.Workdir.URIToPath(change.TextDocumentEdit.TextDocument.URI) + if int32(e.buffers[path].version) != change.TextDocumentEdit.TextDocument.Version { + // Skip edits for old versions. + continue + } + edits := convertEdits(change.TextDocumentEdit.Edits) + if err := e.EditBuffer(ctx, path, edits); err != nil { + return fmt.Errorf("editing buffer %q: %w", path, err) + } } } // Execute any commands. The specification says that commands are @@ -1054,7 +1070,7 @@ func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens return nil, fmt.Errorf("buffer %q is not open", path) } params := &protocol.CodeLensParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), } lens, err := e.Server.CodeLens(ctx, params) if err != nil { @@ -1076,7 +1092,7 @@ func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protoco } params := &protocol.CompletionParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), Position: pos.ToProtocolPosition(), }, } @@ -1114,7 +1130,29 @@ func (e *Editor) Symbols(ctx context.Context, sym string) ([]protocol.SymbolInfo return ans, err } -// References executes a reference request on the server. +// CodeLens executes a codelens request on the server. +func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHint, error) { + if e.Server == nil { + return nil, nil + } + e.mu.Lock() + _, ok := e.buffers[path] + e.mu.Unlock() + if !ok { + return nil, fmt.Errorf("buffer %q is not open", path) + } + params := &protocol.InlayHintParams{ + TextDocument: e.TextDocumentIdentifier(path), + } + hints, err := e.Server.InlayHint(ctx, params) + if err != nil { + return nil, err + } + return hints, nil +} + +// References returns references to the object at (path, pos), as returned by +// the connected LSP server. If no server is connected, it returns (nil, nil). func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { if e.Server == nil { return nil, nil @@ -1127,7 +1165,7 @@ func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protoc } params := &protocol.ReferenceParams{ TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), Position: pos.ToProtocolPosition(), }, Context: protocol.ReferenceContext{ @@ -1141,12 +1179,23 @@ func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protoc return locations, nil } +// Rename performs a rename of the object at (path, pos) to newName, using the +// connected LSP server. If no server is connected, it returns nil. func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName string) error { if e.Server == nil { return nil } + + // Verify that PrepareRename succeeds. + prepareParams := &protocol.PrepareRenameParams{} + prepareParams.TextDocument = e.TextDocumentIdentifier(path) + prepareParams.Position = pos.ToProtocolPosition() + if _, err := e.Server.PrepareRename(ctx, prepareParams); err != nil { + return fmt.Errorf("preparing rename: %v", err) + } + params := &protocol.RenameParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), Position: pos.ToProtocolPosition(), NewName: newName, } @@ -1155,14 +1204,116 @@ func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName strin return err } for _, change := range wsEdits.DocumentChanges { - if err := e.applyProtocolEdit(ctx, change); err != nil { + if err := e.applyDocumentChange(ctx, change); err != nil { return err } } return nil } -func (e *Editor) applyProtocolEdit(ctx context.Context, change protocol.TextDocumentEdit) error { +// Implementations returns implementations for the object at (path, pos), as +// returned by the connected LSP server. If no server is connected, it returns +// (nil, nil). +func (e *Editor) Implementations(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { + if e.Server == nil { + return nil, nil + } + e.mu.Lock() + _, ok := e.buffers[path] + e.mu.Unlock() + if !ok { + return nil, fmt.Errorf("buffer %q is not open", path) + } + params := &protocol.ImplementationParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: e.TextDocumentIdentifier(path), + Position: pos.ToProtocolPosition(), + }, + } + return e.Server.Implementation(ctx, params) +} + +func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error { + closed, opened, err := e.renameBuffers(ctx, oldPath, newPath) + if err != nil { + return err + } + + for _, c := range closed { + if err := e.sendDidClose(ctx, c); err != nil { + return err + } + } + for _, o := range opened { + if err := e.sendDidOpen(ctx, o); err != nil { + return err + } + } + + // Finally, perform the renaming on disk. + if err := e.sandbox.Workdir.RenameFile(ctx, oldPath, newPath); err != nil { + return fmt.Errorf("renaming sandbox file: %w", err) + } + return nil +} + +// renameBuffers renames in-memory buffers affected by the renaming of +// oldPath->newPath, returning the resulting text documents that must be closed +// and opened over the LSP. +func (e *Editor) renameBuffers(ctx context.Context, oldPath, newPath string) (closed []protocol.TextDocumentIdentifier, opened []protocol.TextDocumentItem, _ error) { + e.mu.Lock() + defer e.mu.Unlock() + + // In case either oldPath or newPath is absolute, convert to absolute paths + // before checking for containment. + oldAbs := e.sandbox.Workdir.AbsPath(oldPath) + newAbs := e.sandbox.Workdir.AbsPath(newPath) + + // Collect buffers that are affected by the given file or directory renaming. + buffersToRename := make(map[string]string) // old path -> new path + + for path := range e.buffers { + abs := e.sandbox.Workdir.AbsPath(path) + if oldAbs == abs || source.InDirLex(oldAbs, abs) { + rel, err := filepath.Rel(oldAbs, abs) + if err != nil { + return nil, nil, fmt.Errorf("filepath.Rel(%q, %q): %v", oldAbs, abs, err) + } + nabs := filepath.Join(newAbs, rel) + newPath := e.sandbox.Workdir.RelPath(nabs) + buffersToRename[path] = newPath + } + } + + // Update buffers, and build protocol changes. + for old, new := range buffersToRename { + buf := e.buffers[old] + delete(e.buffers, old) + buf.version = 1 + buf.path = new + e.buffers[new] = buf + + closed = append(closed, e.TextDocumentIdentifier(old)) + opened = append(opened, e.textDocumentItem(buf)) + } + + return closed, opened, nil +} + +func (e *Editor) applyDocumentChange(ctx context.Context, change protocol.DocumentChanges) error { + if change.RenameFile != nil { + oldPath := e.sandbox.Workdir.URIToPath(change.RenameFile.OldURI) + newPath := e.sandbox.Workdir.URIToPath(change.RenameFile.NewURI) + + return e.RenameFile(ctx, oldPath, newPath) + } + if change.TextDocumentEdit != nil { + return e.applyTextDocumentEdit(ctx, *change.TextDocumentEdit) + } + panic("Internal error: one of RenameFile or TextDocumentEdit must be set") +} + +func (e *Editor) applyTextDocumentEdit(ctx context.Context, change protocol.TextDocumentEdit) error { path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) if ver := int32(e.BufferVersion(path)); ver != change.TextDocument.Version { return fmt.Errorf("buffer versions for %q do not match: have %d, editing %d", path, ver, change.TextDocument.Version) @@ -1184,6 +1335,78 @@ func (e *Editor) applyProtocolEdit(ctx context.Context, change protocol.TextDocu return e.EditBuffer(ctx, path, fakeEdits) } +// Config returns the current editor configuration. +func (e *Editor) Config() EditorConfig { + e.mu.Lock() + defer e.mu.Unlock() + return e.config +} + +// ChangeConfiguration sets the new editor configuration, and if applicable +// sends a didChangeConfiguration notification. +// +// An error is returned if the change notification failed to send. +func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error { + e.mu.Lock() + e.config = newConfig + e.mu.Unlock() // don't hold e.mu during server calls + if e.Server != nil { + var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field + if err := e.Server.DidChangeConfiguration(ctx, ¶ms); err != nil { + return err + } + } + return nil +} + +// ChangeWorkspaceFolders sets the new workspace folders, and sends a +// didChangeWorkspaceFolders notification to the server. +// +// The given folders must all be unique. +func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error { + // capture existing folders so that we can compute the change. + e.mu.Lock() + oldFolders := e.makeWorkspaceFoldersLocked() + e.config.WorkspaceFolders = folders + newFolders := e.makeWorkspaceFoldersLocked() + e.mu.Unlock() + + if e.Server == nil { + return nil + } + + var params protocol.DidChangeWorkspaceFoldersParams + + // Keep track of old workspace folders that must be removed. + toRemove := make(map[protocol.URI]protocol.WorkspaceFolder) + for _, folder := range oldFolders { + toRemove[folder.URI] = folder + } + + // Sanity check: if we see a folder twice the algorithm below doesn't work, + // so track seen folders to ensure that we panic in that case. + seen := make(map[protocol.URI]protocol.WorkspaceFolder) + for _, folder := range newFolders { + if _, ok := seen[folder.URI]; ok { + panic(fmt.Sprintf("folder %s seen twice", folder.URI)) + } + + // If this folder already exists, we don't want to remove it. + // Otherwise, we need to add it. + if _, ok := toRemove[folder.URI]; ok { + delete(toRemove, folder.URI) + } else { + params.Event.Added = append(params.Event.Added, folder) + } + } + + for _, v := range toRemove { + params.Event.Removed = append(params.Event.Removed, v) + } + + return e.Server.DidChangeWorkspaceFolders(ctx, ¶ms) +} + // CodeAction executes a codeAction request on the server. func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { if e.Server == nil { @@ -1196,7 +1419,7 @@ func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Rang return nil, fmt.Errorf("buffer %q is not open", path) } params := &protocol.CodeActionParams{ - TextDocument: e.textDocumentIdentifier(path), + TextDocument: e.TextDocumentIdentifier(path), Context: protocol.CodeActionContext{ Diagnostics: diagnostics, }, diff --git a/internal/lsp/fake/editor_test.go b/gopls/internal/lsp/fake/editor_test.go similarity index 100% rename from internal/lsp/fake/editor_test.go rename to gopls/internal/lsp/fake/editor_test.go diff --git a/internal/lsp/fake/proxy.go b/gopls/internal/lsp/fake/proxy.go similarity index 100% rename from internal/lsp/fake/proxy.go rename to gopls/internal/lsp/fake/proxy.go diff --git a/internal/lsp/fake/sandbox.go b/gopls/internal/lsp/fake/sandbox.go similarity index 88% rename from internal/lsp/fake/sandbox.go rename to gopls/internal/lsp/fake/sandbox.go index b4395646bc6..206a3de1ba6 100644 --- a/internal/lsp/fake/sandbox.go +++ b/gopls/internal/lsp/fake/sandbox.go @@ -13,6 +13,7 @@ import ( "path/filepath" "strings" + "golang.org/x/tools/gopls/internal/robustio" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" @@ -21,10 +22,11 @@ import ( // Sandbox holds a collection of temporary resources to use for working with Go // code in tests. type Sandbox struct { - gopath string - rootdir string - goproxy string - Workdir *Workdir + gopath string + rootdir string + goproxy string + Workdir *Workdir + goCommandRunner gocommand.Runner } // SandboxConfig controls the behavior of a test sandbox. The zero value @@ -68,6 +70,10 @@ type SandboxConfig struct { // If rootDir is non-empty, it will be used as the root of temporary // directories created for the sandbox. Otherwise, a new temporary directory // will be used as root. +// +// TODO(rfindley): the sandbox abstraction doesn't seem to carry its weight. +// Sandboxes should be composed out of their building-blocks, rather than via a +// monolithic configuration. func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) { if config == nil { config = new(SandboxConfig) @@ -157,6 +163,9 @@ func UnpackTxt(txt string) map[string][]byte { dataMap := make(map[string][]byte) archive := txtar.Parse([]byte(txt)) for _, f := range archive.Files { + if _, ok := dataMap[f.Name]; ok { + panic(fmt.Sprintf("found file %q twice", f.Name)) + } dataMap[f.Name] = f.Data } return dataMap @@ -220,30 +229,36 @@ func (sb *Sandbox) GoEnv() map[string]string { return vars } -// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is -// true, the sandbox scans the working directory and emits file change events -// for any file changes it finds. -func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error { +// goCommandInvocation returns a new gocommand.Invocation initialized with the +// sandbox environment variables and working directory. +func (sb *Sandbox) goCommandInvocation() gocommand.Invocation { var vars []string for k, v := range sb.GoEnv() { vars = append(vars, fmt.Sprintf("%s=%s", k, v)) } inv := gocommand.Invocation{ - Verb: verb, - Args: args, - Env: vars, + Env: vars, } - // Use the provided directory for the working directory, if available. // sb.Workdir may be nil if we exited the constructor with errors (we call // Close to clean up any partial state from the constructor, which calls // RunGoCommand). + if sb.Workdir != nil { + inv.WorkingDir = string(sb.Workdir.RelativeTo) + } + return inv +} + +// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is +// true, the sandbox scans the working directory and emits file change events +// for any file changes it finds. +func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []string, checkForFileChanges bool) error { + inv := sb.goCommandInvocation() + inv.Verb = verb + inv.Args = args if dir != "" { inv.WorkingDir = sb.Workdir.AbsPath(dir) - } else if sb.Workdir != nil { - inv.WorkingDir = string(sb.Workdir.RelativeTo) } - gocmdRunner := &gocommand.Runner{} - stdout, stderr, _, err := gocmdRunner.RunRaw(ctx, inv) + stdout, stderr, _, err := sb.goCommandRunner.RunRaw(ctx, inv) if err != nil { return fmt.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err) } @@ -260,13 +275,20 @@ func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args []st return nil } +// GoVersion checks the version of the go command. +// It returns the X in Go 1.X. +func (sb *Sandbox) GoVersion(ctx context.Context) (int, error) { + inv := sb.goCommandInvocation() + return gocommand.GoVersion(ctx, inv, &sb.goCommandRunner) +} + // Close removes all state associated with the sandbox. func (sb *Sandbox) Close() error { var goCleanErr error if sb.gopath != "" { goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"}, false) } - err := os.RemoveAll(sb.rootdir) + err := robustio.RemoveAll(sb.rootdir) if err != nil || goCleanErr != nil { return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err) } diff --git a/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go similarity index 88% rename from internal/lsp/fake/workdir.go rename to gopls/internal/lsp/fake/workdir.go index 734f5fd8197..2b426e40c3b 100644 --- a/internal/lsp/fake/workdir.go +++ b/gopls/internal/lsp/fake/workdir.go @@ -13,12 +13,14 @@ import ( "os" "path/filepath" "runtime" + "sort" "strings" "sync" "time" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/robustio" + "golang.org/x/tools/gopls/internal/span" ) // FileEvent wraps the protocol.FileEvent so that it can be associated with a @@ -62,6 +64,8 @@ func WriteFileData(path string, content []byte, rel RelativeTo) error { for { err := ioutil.WriteFile(fp, []byte(content), 0644) if err != nil { + // This lock file violation is not handled by the robustio package, as it + // indicates a real race condition that could be avoided. if isWindowsErrLockViolation(err) { time.Sleep(backoff) backoff *= 2 @@ -311,13 +315,55 @@ func (w *Workdir) writeFile(ctx context.Context, path, content string) (FileEven if err := WriteFileData(path, []byte(content), w.RelativeTo); err != nil { return FileEvent{}, err } + return w.fileEvent(path, changeType), nil +} + +func (w *Workdir) fileEvent(path string, changeType protocol.FileChangeType) FileEvent { return FileEvent{ Path: path, ProtocolEvent: protocol.FileEvent{ URI: w.URI(path), Type: changeType, }, - }, nil + } +} + +// RenameFile performs an on disk-renaming of the workdir-relative oldPath to +// workdir-relative newPath. +func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error { + oldAbs := w.AbsPath(oldPath) + newAbs := w.AbsPath(newPath) + + if err := robustio.Rename(oldAbs, newAbs); err != nil { + return err + } + + // Send synthetic file events for the renaming. Renamed files are handled as + // Delete+Create events: + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#fileChangeType + events := []FileEvent{ + w.fileEvent(oldPath, protocol.Deleted), + w.fileEvent(newPath, protocol.Created), + } + w.sendEvents(ctx, events) + + return nil +} + +// ListFiles returns a new sorted list of the relative paths of files in dir, +// recursively. +func (w *Workdir) ListFiles(dir string) ([]string, error) { + m, err := w.listFiles(dir) + if err != nil { + return nil, err + } + + var paths []string + for p := range m { + paths = append(paths, p) + } + sort.Strings(paths) + return paths, nil } // listFiles lists files in the given directory, returning a map of relative diff --git a/internal/lsp/fake/workdir_test.go b/gopls/internal/lsp/fake/workdir_test.go similarity index 98% rename from internal/lsp/fake/workdir_test.go rename to gopls/internal/lsp/fake/workdir_test.go index 33fbb9fa1d5..77c6684556c 100644 --- a/internal/lsp/fake/workdir_test.go +++ b/gopls/internal/lsp/fake/workdir_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) const data = ` diff --git a/internal/lsp/fake/workdir_windows.go b/gopls/internal/lsp/fake/workdir_windows.go similarity index 70% rename from internal/lsp/fake/workdir_windows.go rename to gopls/internal/lsp/fake/workdir_windows.go index bcd18b7a226..4d4f0152764 100644 --- a/internal/lsp/fake/workdir_windows.go +++ b/gopls/internal/lsp/fake/workdir_windows.go @@ -10,8 +10,10 @@ import ( ) func init() { - // from https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- - const ERROR_LOCK_VIOLATION syscall.Errno = 33 + // constants copied from GOROOT/src/internal/syscall/windows/syscall_windows.go + const ( + ERROR_LOCK_VIOLATION syscall.Errno = 33 + ) isWindowsErrLockViolation = func(err error) bool { return errors.Is(err, ERROR_LOCK_VIOLATION) diff --git a/internal/lsp/folding_range.go b/gopls/internal/lsp/folding_range.go similarity index 92% rename from internal/lsp/folding_range.go rename to gopls/internal/lsp/folding_range.go index 75f48a4498f..4a2d828e995 100644 --- a/internal/lsp/folding_range.go +++ b/gopls/internal/lsp/folding_range.go @@ -7,8 +7,8 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) { diff --git a/internal/lsp/format.go b/gopls/internal/lsp/format.go similarity index 78% rename from internal/lsp/format.go rename to gopls/internal/lsp/format.go index 19736af38bc..773a4690e92 100644 --- a/internal/lsp/format.go +++ b/gopls/internal/lsp/format.go @@ -7,10 +7,10 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/work" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/work" ) func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { diff --git a/internal/lsp/general.go b/gopls/internal/lsp/general.go similarity index 73% rename from internal/lsp/general.go rename to gopls/internal/lsp/general.go index 478152bdf9a..57348cd564b 100644 --- a/internal/lsp/general.go +++ b/gopls/internal/lsp/general.go @@ -13,15 +13,17 @@ import ( "os" "path" "path/filepath" + "sort" + "strings" "sync" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" ) func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { @@ -137,6 +139,7 @@ See https://github.com/golang/go/issues/45732 for more information.`, Capabilities: protocol.ServerCapabilities{ CallHierarchyProvider: true, CodeActionProvider: codeActionProvider, + CodeLensProvider: &protocol.CodeLensOptions{}, // must be non-nil to enable the code lens capability CompletionProvider: protocol.CompletionOptions{ TriggerCharacters: []string{"."}, }, @@ -153,6 +156,7 @@ See https://github.com/golang/go/issues/45732 for more information.`, HoverProvider: true, DocumentHighlightProvider: true, DocumentLinkProvider: protocol.DocumentLinkOptions{}, + InlayHintProvider: protocol.InlayHintOptions{}, ReferencesProvider: true, RenameProvider: renameOpts, SignatureHelpProvider: protocol.SignatureHelpOptions{ @@ -172,10 +176,7 @@ See https://github.com/golang/go/issues/45732 for more information.`, }, }, }, - ServerInfo: struct { - Name string `json:"name"` - Version string `json:"version,omitempty"` - }{ + ServerInfo: protocol.PServerInfoMsg_initialize{ Name: "gopls", Version: string(goplsVersion), }, @@ -203,6 +204,7 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa return err } s.pendingFolders = nil + s.checkViewGoVersions() var registrations []protocol.Registration if options.ConfigurationSupported && options.DynamicConfigurationSupported { @@ -224,22 +226,100 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa return nil } +// GoVersionTable maps Go versions to the gopls version in which support will +// be deprecated, and the final gopls version supporting them without warnings. +// Keep this in sync with gopls/README.md +// +// Must be sorted in ascending order of Go version. +// +// Mutable for testing. +var GoVersionTable = []GoVersionSupport{ + {12, "", "v0.7.5"}, + {15, "v0.11.0", "v0.9.5"}, +} + +// GoVersionSupport holds information about end-of-life Go version support. +type GoVersionSupport struct { + GoVersion int + DeprecatedVersion string // if unset, the version is already deprecated + InstallGoplsVersion string +} + +// OldestSupportedGoVersion is the last X in Go 1.X that this version of gopls +// supports. +func OldestSupportedGoVersion() int { + return GoVersionTable[len(GoVersionTable)-1].GoVersion + 1 +} + +// versionMessage returns the warning/error message to display if the user is +// on the given Go version, if any. The goVersion variable is the X in Go 1.X. +// +// If goVersion is invalid (< 0), it returns "", 0. +func versionMessage(goVersion int) (string, protocol.MessageType) { + if goVersion < 0 { + return "", 0 + } + + for _, v := range GoVersionTable { + if goVersion <= v.GoVersion { + var msgBuilder strings.Builder + + mType := protocol.Error + fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", goVersion) + if v.DeprecatedVersion != "" { + // not deprecated yet, just a warning + fmt.Fprintf(&msgBuilder, ", which will be unsupported by gopls %s. ", v.DeprecatedVersion) + mType = protocol.Warning + } else { + fmt.Fprint(&msgBuilder, ", which is not supported by this version of gopls. ") + } + fmt.Fprintf(&msgBuilder, "Please upgrade to Go 1.%d or later and reinstall gopls. ", OldestSupportedGoVersion()) + fmt.Fprintf(&msgBuilder, "If you can't upgrade and want this message to go away, please install gopls %s. ", v.InstallGoplsVersion) + fmt.Fprint(&msgBuilder, "See https://go.dev/s/gopls-support-policy for more details.") + + return msgBuilder.String(), mType + } + } + return "", 0 +} + +// checkViewGoVersions checks whether any Go version used by a view is too old, +// raising a showMessage notification if so. +// +// It should be called after views change. +func (s *Server) checkViewGoVersions() { + oldestVersion := -1 + for _, view := range s.session.Views() { + viewVersion := view.GoVersion() + if oldestVersion == -1 || viewVersion < oldestVersion { + oldestVersion = viewVersion + } + } + + if msg, mType := versionMessage(oldestVersion); msg != "" { + s.eventuallyShowMessage(context.Background(), &protocol.ShowMessageParams{ + Type: mType, + Message: msg, + }) + } +} + func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) error { originalViews := len(s.session.Views()) viewErrors := make(map[span.URI]error) - var wg sync.WaitGroup + var ndiagnose sync.WaitGroup // number of unfinished diagnose calls if s.session.Options().VerboseWorkDoneProgress { work := s.progress.Start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil) defer func() { go func() { - wg.Wait() + ndiagnose.Wait() work.End(ctx, "Done.") }() }() } // Only one view gets to have a workspace. - var allFoldersWg sync.WaitGroup + var nsnapshots sync.WaitGroup // number of unfinished snapshot initializations for _, folder := range folders { uri := span.URIFromURI(folder.URI) // Ignore non-file URIs. @@ -248,46 +328,50 @@ func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol } work := s.progress.Start(ctx, "Setting up workspace", "Loading packages...", nil, nil) snapshot, release, err := s.addView(ctx, folder.Name, uri) - if err == source.ErrViewExists { - continue - } if err != nil { + if err == source.ErrViewExists { + continue + } viewErrors[uri] = err work.End(ctx, fmt.Sprintf("Error loading packages: %s", err)) continue } - var swg sync.WaitGroup - swg.Add(1) - allFoldersWg.Add(1) - go func() { - defer swg.Done() - defer allFoldersWg.Done() - snapshot.AwaitInitialized(ctx) - work.End(ctx, "Finished loading packages.") - }() + // Inv: release() must be called once. // Print each view's environment. - buf := &bytes.Buffer{} - if err := snapshot.WriteEnv(ctx, buf); err != nil { + var buf bytes.Buffer + if err := snapshot.WriteEnv(ctx, &buf); err != nil { viewErrors[uri] = err + release() continue } event.Log(ctx, buf.String()) - // Diagnose the newly created view. - wg.Add(1) + // Initialize snapshot asynchronously. + initialized := make(chan struct{}) + nsnapshots.Add(1) + go func() { + snapshot.AwaitInitialized(ctx) + work.End(ctx, "Finished loading packages.") + nsnapshots.Done() + close(initialized) // signal + }() + + // Diagnose the newly created view asynchronously. + ndiagnose.Add(1) go func() { s.diagnoseDetached(snapshot) - swg.Wait() + <-initialized release() - wg.Done() + ndiagnose.Done() }() } + // Wait for snapshots to be initialized so that all files are known. + // (We don't need to wait for diagnosis to finish.) + nsnapshots.Wait() + // Register for file watching notifications, if they are supported. - // Wait for all snapshots to be initialized first, since all files might - // not yet be known to the snapshots. - allFoldersWg.Wait() if err := s.updateWatchedDirectories(ctx); err != nil { event.Error(ctx, "failed to register for file watching notifications", err) } @@ -398,13 +482,12 @@ func (s *Server) fetchConfig(ctx context.Context, name string, folder span.URI, return nil } configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{ - ConfigurationParams: protocol.ConfigurationParams{ - Items: []protocol.ConfigurationItem{{ - ScopeURI: string(folder), - Section: "gopls", - }}, - }, - }) + Items: []protocol.ConfigurationItem{{ + ScopeURI: string(folder), + Section: "gopls", + }}, + }, + ) if err != nil { return fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err) } @@ -427,28 +510,46 @@ func (s *Server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMe } func (s *Server) handleOptionResults(ctx context.Context, results source.OptionResults) error { + var warnings, errors []string for _, result := range results { - var msg *protocol.ShowMessageParams switch result.Error.(type) { case nil: // nothing to do case *source.SoftError: - msg = &protocol.ShowMessageParams{ - Type: protocol.Warning, - Message: result.Error.Error(), - } + warnings = append(warnings, result.Error.Error()) default: - msg = &protocol.ShowMessageParams{ - Type: protocol.Error, - Message: result.Error.Error(), - } + errors = append(errors, result.Error.Error()) } - if msg != nil { - if err := s.eventuallyShowMessage(ctx, msg); err != nil { - return err - } + } + + // Sort messages, but put errors first. + // + // Having stable content for the message allows clients to de-duplicate. This + // matters because we may send duplicate warnings for clients that support + // dynamic configuration: one for the initial settings, and then more for the + // individual view settings. + var msgs []string + msgType := protocol.Warning + if len(errors) > 0 { + msgType = protocol.Error + sort.Strings(errors) + msgs = append(msgs, errors...) + } + if len(warnings) > 0 { + sort.Strings(warnings) + msgs = append(msgs, warnings...) + } + + if len(msgs) > 0 { + // Settings + combined := "Invalid settings: " + strings.Join(msgs, "; ") + params := &protocol.ShowMessageParams{ + Type: msgType, + Message: combined, } + return s.eventuallyShowMessage(ctx, params) } + return nil } @@ -473,8 +574,7 @@ func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI release() return nil, nil, false, func() {}, err } - kind := snapshot.View().FileKind(fh) - if expectKind != source.UnknownKind && kind != expectKind { + if expectKind != source.UnknownKind && view.FileKind(fh) != expectKind { // Wrong kind of file. Nothing to do. release() return nil, nil, false, func() {}, nil @@ -482,6 +582,8 @@ func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI return snapshot, fh, true, release, nil } +// 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 { s.stateMu.Lock() defer s.stateMu.Unlock() diff --git a/gopls/internal/lsp/general_test.go b/gopls/internal/lsp/general_test.go new file mode 100644 index 00000000000..a0312ba1b43 --- /dev/null +++ b/gopls/internal/lsp/general_test.go @@ -0,0 +1,44 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lsp + +import ( + "strings" + "testing" + + "golang.org/x/tools/gopls/internal/lsp/protocol" +) + +func TestVersionMessage(t *testing.T) { + tests := []struct { + goVersion int + wantContains []string // string fragments that we expect to see + wantType protocol.MessageType + }{ + {-1, nil, 0}, + {12, []string{"1.12", "not supported", "upgrade to Go 1.16", "install gopls v0.7.5"}, protocol.Error}, + {13, []string{"1.13", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning}, + {15, []string{"1.15", "will be unsupported by gopls v0.11.0", "upgrade to Go 1.16", "install gopls v0.9.5"}, protocol.Warning}, + {16, nil, 0}, + } + + for _, test := range tests { + gotMsg, gotType := versionMessage(test.goVersion) + + if len(test.wantContains) == 0 && gotMsg != "" { + t.Errorf("versionMessage(%d) = %q, want \"\"", test.goVersion, gotMsg) + } + + for _, want := range test.wantContains { + if !strings.Contains(gotMsg, want) { + t.Errorf("versionMessage(%d) = %q, want containing %q", test.goVersion, gotMsg, want) + } + } + + if gotType != test.wantType { + t.Errorf("versionMessage(%d) = returned message type %d, want %d", test.goVersion, gotType, test.wantType) + } + } +} diff --git a/internal/lsp/helper/README.md b/gopls/internal/lsp/helper/README.md similarity index 100% rename from internal/lsp/helper/README.md rename to gopls/internal/lsp/helper/README.md diff --git a/internal/lsp/helper/helper.go b/gopls/internal/lsp/helper/helper.go similarity index 99% rename from internal/lsp/helper/helper.go rename to gopls/internal/lsp/helper/helper.go index cadda0246be..391d75adef0 100644 --- a/internal/lsp/helper/helper.go +++ b/gopls/internal/lsp/helper/helper.go @@ -56,7 +56,7 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) {{range $key, $v := .Stuff}} diff --git a/internal/lsp/highlight.go b/gopls/internal/lsp/highlight.go similarity index 85% rename from internal/lsp/highlight.go rename to gopls/internal/lsp/highlight.go index 5dc636eb58a..290444ec962 100644 --- a/internal/lsp/highlight.go +++ b/gopls/internal/lsp/highlight.go @@ -8,10 +8,10 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" ) func (s *Server) documentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) { diff --git a/internal/lsp/hover.go b/gopls/internal/lsp/hover.go similarity index 77% rename from internal/lsp/hover.go rename to gopls/internal/lsp/hover.go index d59f5dbdb3b..2d1aae7d5b5 100644 --- a/internal/lsp/hover.go +++ b/gopls/internal/lsp/hover.go @@ -7,11 +7,11 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/mod" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" - "golang.org/x/tools/internal/lsp/work" + "golang.org/x/tools/gopls/internal/lsp/mod" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/work" ) func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { diff --git a/internal/lsp/implementation.go b/gopls/internal/lsp/implementation.go similarity index 84% rename from internal/lsp/implementation.go rename to gopls/internal/lsp/implementation.go index 49992b9113a..0eb82652e9e 100644 --- a/internal/lsp/implementation.go +++ b/gopls/internal/lsp/implementation.go @@ -7,8 +7,8 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) { diff --git a/gopls/internal/lsp/inlay_hint.go b/gopls/internal/lsp/inlay_hint.go new file mode 100644 index 00000000000..6aceecb0d33 --- /dev/null +++ b/gopls/internal/lsp/inlay_hint.go @@ -0,0 +1,21 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lsp + +import ( + "context" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" +) + +func (s *Server) inlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { + snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) + defer release() + if !ok { + return nil, err + } + return source.InlayHint(ctx, snapshot, fh, params.Range) +} diff --git a/internal/lsp/link.go b/gopls/internal/lsp/link.go similarity index 76% rename from internal/lsp/link.go rename to gopls/internal/lsp/link.go index 7bb09b40355..b26bfb33c80 100644 --- a/internal/lsp/link.go +++ b/gopls/internal/lsp/link.go @@ -17,11 +17,11 @@ import ( "sync" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/event/tag" ) func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) (links []protocol.DocumentLink, err error) { @@ -49,6 +49,8 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl if err != nil { return nil, err } + tokFile := pm.Mapper.TokFile + var links []protocol.DocumentLink for _, req := range pm.File.Require { if req.Syntax == nil { @@ -66,9 +68,9 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl } // Shift the start position to the location of the // dependency within the require statement. - start, end := token.Pos(s+i), token.Pos(s+i+len(dep)) + start, end := tokFile.Pos(s+i), tokFile.Pos(s+i+len(dep)) target := source.BuildLink(snapshot.View().Options().LinkTarget, "mod/"+req.Mod.String(), "") - l, err := toProtocolLink(snapshot, pm.Mapper, target, start, end, source.Mod) + l, err := toProtocolLink(tokFile, pm.Mapper, target, start, end) if err != nil { return nil, err } @@ -78,6 +80,7 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl if syntax := pm.File.Syntax; syntax == nil { return links, nil } + // Get all the links that are contained in the comments of the file. for _, expr := range pm.File.Syntax.Stmt { comments := expr.Comment() @@ -86,7 +89,8 @@ func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandl } for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} { for _, comment := range section { - l, err := findLinksInString(ctx, snapshot, comment.Token, token.Pos(comment.Start.Byte), pm.Mapper, source.Mod) + start := tokFile.Pos(comment.Start.Byte) + l, err := findLinksInString(ctx, snapshot, comment.Token, start, tokFile, pm.Mapper) if err != nil { return nil, err } @@ -143,8 +147,8 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle // Account for the quotation marks in the positions. start := imp.Path.Pos() + 1 end := imp.Path.End() - 1 - target = source.BuildLink(view.Options().LinkTarget, target, "") - l, err := toProtocolLink(snapshot, pgf.Mapper, target, start, end, source.Go) + targetURL := source.BuildLink(view.Options().LinkTarget, target, "") + l, err := toProtocolLink(pgf.Tok, pgf.Mapper, targetURL, start, end) if err != nil { return nil, err } @@ -152,7 +156,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle } } for _, s := range str { - l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Mapper, source.Go) + l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Tok, pgf.Mapper) if err != nil { return nil, err } @@ -160,7 +164,7 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle } for _, commentGroup := range pgf.File.Comments { for _, comment := range commentGroup.List { - l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Mapper, source.Go) + l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Tok, pgf.Mapper) if err != nil { return nil, err } @@ -170,8 +174,8 @@ func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle return links, nil } -func moduleAtVersion(target string, pkg source.Package) (string, string, bool) { - impPkg, err := pkg.GetImport(target) +func moduleAtVersion(targetImportPath string, pkg source.Package) (string, string, bool) { + impPkg, err := pkg.ResolveImportPath(targetImportPath) if err != nil { return "", "", false } @@ -193,7 +197,8 @@ var acceptedSchemes = map[string]bool{ "https": true, } -func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, m *protocol.ColumnMapper, fileKind source.FileKind) ([]protocol.DocumentLink, error) { +// tokFile may be a throwaway File for non-Go files. +func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, tokFile *token.File, m *protocol.ColumnMapper) ([]protocol.DocumentLink, error) { var links []protocol.DocumentLink for _, index := range snapshot.View().Options().URLRegexp.FindAllIndex([]byte(src), -1) { start, end := index[0], index[1] @@ -216,7 +221,7 @@ func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string if !acceptedSchemes[linkURL.Scheme] { continue } - l, err := toProtocolLink(snapshot, m, linkURL.String(), startPos, endPos, fileKind) + l, err := toProtocolLink(tokFile, m, linkURL.String(), startPos, endPos) if err != nil { return nil, err } @@ -233,8 +238,8 @@ func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string continue } org, repo, number := matches[1], matches[2], matches[3] - target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) - l, err := toProtocolLink(snapshot, m, target, startPos, endPos, fileKind) + targetURL := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) + l, err := toProtocolLink(tokFile, m, targetURL, startPos, endPos) if err != nil { return nil, err } @@ -255,37 +260,17 @@ var ( issueRegexp *regexp.Regexp ) -func toProtocolLink(snapshot source.Snapshot, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) { - var rng protocol.Range - switch fileKind { - case source.Go: - spn, err := span.NewRange(snapshot.FileSet(), start, end).Span() - if err != nil { - return protocol.DocumentLink{}, err - } - rng, err = m.Range(spn) - if err != nil { - return protocol.DocumentLink{}, err - } - case source.Mod: - s, e := int(start), int(end) - line, col, err := span.ToPosition(m.TokFile, s) - if err != nil { - return protocol.DocumentLink{}, err - } - start := span.NewPoint(line, col, s) - line, col, err = span.ToPosition(m.TokFile, e) - if err != nil { - return protocol.DocumentLink{}, err - } - end := span.NewPoint(line, col, e) - rng, err = m.Range(span.New(m.URI, start, end)) - if err != nil { - return protocol.DocumentLink{}, err - } +func toProtocolLink(tokFile *token.File, m *protocol.ColumnMapper, targetURL string, start, end token.Pos) (protocol.DocumentLink, error) { + spn, err := span.NewRange(tokFile, start, end).Span() + if err != nil { + return protocol.DocumentLink{}, err + } + rng, err := m.Range(spn) + if err != nil { + return protocol.DocumentLink{}, err } return protocol.DocumentLink{ Range: rng, - Target: target, + Target: targetURL, }, nil } diff --git a/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go similarity index 83% rename from internal/lsp/lsp_test.go rename to gopls/internal/lsp/lsp_test.go index ee364b8b034..d966d94397d 100644 --- a/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -15,21 +15,31 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { bug.PanicOnBugs = true testenv.ExitIfSmallMachine() + + // Set the global exporter to nil so that we don't log to stderr. This avoids + // a lot of misleading noise in test output. + // + // TODO(rfindley): investigate whether we can/should capture logs scoped to + // individual tests by passing in a context with a local exporter. + event.SetExporter(nil) + os.Exit(m.Run()) } @@ -49,7 +59,7 @@ type runner struct { func testLSP(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) session := cache.NewSession(ctx) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) @@ -67,6 +77,9 @@ func testLSP(t *testing.T, datum *tests.Data) { tests.EnableAllAnalyzers(view, options) view.SetOptions(ctx, options) + // Enable all inlay hints for tests. + tests.EnableAllInlayHints(view, options) + // Only run the -modfile specific tests in module mode with Go 1.14 or above. datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14 release() @@ -208,19 +221,8 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost // Get the diagnostics for this view if we have not done it before. v := r.server.session.View(r.data.Config.Dir) r.collectDiagnostics(v) - d := r.diagnostics[uri] - got := make([]*source.Diagnostic, len(d)) - copy(got, d) - // A special case to test that there are no diagnostics for a file. - if len(want) == 1 && want[0].Source == "no_diagnostics" { - if len(got) != 0 { - t.Errorf("expected no diagnostics for %s, got %v", uri, got) - } - return - } - if diff := tests.DiffDiagnostics(uri, want, got); diff != "" { - t.Error(diff) - } + got := append([]*source.Diagnostic(nil), r.diagnostics[uri]...) // copy + tests.CompareDiagnostics(t, uri, want, got) } func (r *runner) FoldingRanges(t *testing.T, spn span.Span) { @@ -284,7 +286,7 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges continue } tag := fmt.Sprintf("%s-%d", prefix, i) - want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -311,7 +313,7 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges continue } tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i) - want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -358,26 +360,18 @@ func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.Fol res := contents // Apply the edits from the end of the file forward // to preserve the offsets + // TODO(adonovan): factor to use diff.ApplyEdits, which validates the input. for i := len(ranges) - 1; i >= 0; i-- { - fRange := ranges[i] - spn, err := m.RangeSpan(protocol.Range{ - Start: protocol.Position{ - Line: fRange.StartLine, - Character: fRange.StartCharacter, - }, - End: protocol.Position{ - Line: fRange.EndLine, - Character: fRange.EndCharacter, - }, - }) + r := ranges[i] + start, err := m.Point(protocol.Position{r.StartLine, r.StartCharacter}) if err != nil { return "", err } - start := spn.Start().Offset() - end := spn.End().Offset() - - tmp := res[0:start] + foldedText - res = tmp + res[end:] + end, err := m.Point(protocol.Position{r.EndLine, r.EndCharacter}) + if err != nil { + return "", err + } + res = res[:start.Offset()] + foldedText + res[end.Offset():] } return res, nil } @@ -385,7 +379,7 @@ func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.Fol func (r *runner) Format(t *testing.T, spn span.Span) { uri := spn.URI() filename := uri.Filename() - gofmted := string(r.data.Golden("gofmt", filename, func() ([]byte, error) { + gofmted := string(r.data.Golden(t, "gofmt", filename, func() ([]byte, error) { cmd := exec.Command("gofmt", filename) out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files return out, nil @@ -406,13 +400,12 @@ func (r *runner) Format(t *testing.T, spn span.Span) { if err != nil { t.Fatal(err) } - sedits, err := source.FromProtocolEdits(m, edits) + got, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Error(err) } - got := diff.ApplyEdits(string(m.Content), sedits) - if gofmted != got { - t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got) + if diff := compare.Text(gofmted, got); diff != "" { + t.Errorf("format failed for %s (-want +got):\n%s", filename, diff) } } @@ -472,19 +465,16 @@ func (r *runner) Import(t *testing.T, spn span.Span) { } got = res[uri] } - want := string(r.data.Golden("goimports", filename, func() ([]byte, error) { + want := string(r.data.Golden(t, "goimports", filename, func() ([]byte, error) { return []byte(got), nil })) - if want != got { - d, err := myers.ComputeEdits(uri, want, got) - if err != nil { - t.Fatal(err) - } - t.Errorf("import failed for %s: %s", filename, diff.ToUnified("want", "got", want, d)) + + if d := compare.Text(want, got); d != "" { + t.Errorf("import failed for %s:\n%s", filename, d) } } -func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, expectedActions int) { +func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.SuggestedFix, expectedActions int) { uri := spn.URI() view, err := r.server.session.ViewOf(uri) if err != nil { @@ -511,11 +501,11 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, break } } - codeActionKinds := []protocol.CodeActionKind{} + var codeActionKinds []protocol.CodeActionKind for _, k := range actionKinds { - codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k)) + codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k.ActionKind)) } - actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ + allActions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.URIFromSpanURI(uri), }, @@ -528,13 +518,22 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, if err != nil { t.Fatalf("CodeAction %s failed: %v", spn, err) } + var actions []protocol.CodeAction + for _, action := range allActions { + for _, fix := range actionKinds { + if strings.Contains(action.Title, fix.Title) { + actions = append(actions, action) + break + } + } + + } if len(actions) != expectedActions { - // Hack: We assume that we only get one code action per range. - var cmds []string + var summaries []string for _, a := range actions { - cmds = append(cmds, fmt.Sprintf("%s (%s)", a.Command, a.Title)) + summaries = append(summaries, fmt.Sprintf("%q (%s)", a.Title, a.Kind)) } - t.Fatalf("unexpected number of code actions, want %d, got %d: %v", expectedActions, len(actions), cmds) + t.Fatalf("CodeAction(...): got %d code actions (%v), want %d", len(actions), summaries, expectedActions) } action := actions[0] var match bool @@ -545,7 +544,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, } } if !match { - t.Fatalf("unexpected kind for code action %s, expected one of %v, got %v", action.Title, codeActionKinds, action.Kind) + t.Fatalf("unexpected kind for code action %s, got %v, want one of %v", action.Title, action.Kind, codeActionKinds) } var res map[span.URI]string if cmd := action.Command; cmd != nil { @@ -564,11 +563,11 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, } } for u, got := range res { - want := string(r.data.Golden("suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, "suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { return []byte(got), nil })) if want != got { - t.Errorf("suggested fixes failed for %s:\n%s", u.Filename(), tests.Diff(t, want, got)) + t.Errorf("suggested fixes failed for %s:\n%s", u.Filename(), compare.Text(want, got)) } } } @@ -616,11 +615,11 @@ func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span } res := <-r.editRecv for u, got := range res { - want := string(r.data.Golden("functionextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, "functionextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { return []byte(got), nil })) if want != got { - t.Errorf("function extraction failed for %s:\n%s", u.Filename(), tests.Diff(t, want, got)) + t.Errorf("function extraction failed for %s:\n%s", u.Filename(), compare.Text(want, got)) } } } @@ -668,11 +667,11 @@ func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) } res := <-r.editRecv for u, got := range res { - want := string(r.data.Golden("methodextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, "methodextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { return []byte(got), nil })) if want != got { - t.Errorf("method extraction failed for %s:\n%s", u.Filename(), tests.Diff(t, want, got)) + t.Errorf("method extraction failed for %s:\n%s", u.Filename(), compare.Text(want, got)) } } } @@ -720,13 +719,13 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if hover != nil { didSomething = true tag := fmt.Sprintf("%s-hoverdef", d.Name) - expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) { + expectHover := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) { return []byte(hover.Contents.Value), nil })) got := tests.StripSubscripts(hover.Contents.Value) expectHover = tests.StripSubscripts(expectHover) if got != expectHover { - t.Errorf("%s:\n%s", d.Src, tests.Diff(t, expectHover, got)) + tests.CheckSameMarkdown(t, got, expectHover) } } if !d.OnlyHover { @@ -932,6 +931,54 @@ func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) { } } +func (r *runner) InlayHints(t *testing.T, spn span.Span) { + uri := spn.URI() + filename := uri.Filename() + + hints, err := r.server.InlayHint(r.ctx, &protocol.InlayHintParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(uri), + }, + // TODO: add Range + }) + if err != nil { + t.Fatal(err) + } + + // Map inlay hints to text edits. + edits := make([]protocol.TextEdit, len(hints)) + for i, hint := range hints { + var paddingLeft, paddingRight string + if hint.PaddingLeft { + paddingLeft = " " + } + if hint.PaddingRight { + paddingRight = " " + } + edits[i] = protocol.TextEdit{ + Range: protocol.Range{Start: *hint.Position, End: *hint.Position}, + NewText: fmt.Sprintf("<%s%s%s>", paddingLeft, hint.Label[0].Value, paddingRight), + } + } + + m, err := r.data.Mapper(uri) + if err != nil { + t.Fatal(err) + } + got, _, err := source.ApplyProtocolEdits(m, edits) + if err != nil { + t.Error(err) + } + + withinlayHints := string(r.data.Golden(t, "inlayHint", filename, func() ([]byte, error) { + return []byte(got), nil + })) + + if withinlayHints != got { + t.Errorf("inlay hints failed for %s, expected:\n%v\ngot:\n%v", filename, withinlayHints, got) + } +} + func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { tag := fmt.Sprintf("%s-rename", newText) @@ -954,7 +1001,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { NewName: newText, }) if err != nil { - renamed := string(r.data.Golden(tag, filename, func() ([]byte, error) { + renamed := string(r.data.Golden(t, tag, filename, func() ([]byte, error) { return []byte(err.Error()), nil })) if err.Error() != renamed { @@ -984,11 +1031,11 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { val := res[uri] got += val } - want := string(r.data.Golden(tag, filename, func() ([]byte, error) { + want := string(r.data.Golden(t, tag, filename, func() ([]byte, error) { return []byte(got), nil })) if want != got { - t.Errorf("rename failed for %s:\n%s", newText, tests.Diff(t, want, got)) + t.Errorf("rename failed for %s:\n%s", newText, compare.Text(want, got)) } } @@ -1013,7 +1060,12 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare t.Errorf("prepare rename failed for %v: got error: %v", src, err) return } - // we all love typed nils + + // TODO(rfindley): can we consolidate on a single representation for + // PrepareRename results, and use cmp.Diff here? + + // PrepareRename may fail with no error if there was no object found at the + // position. if got == nil { if want.Text != "" { // expected an ident. t.Errorf("prepare rename failed for %v: got nil", src) @@ -1027,7 +1079,7 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare t.Errorf("prepare rename failed: incorrect point, got %v want %v", got.Range.Start, want.Range.Start) } } else { - if protocol.CompareRange(got.Range, want.Range) != 0 { + if got.Range != want.Range { t.Errorf("prepare rename failed: incorrect range got %v want %v", got.Range, want.Range) } } @@ -1036,46 +1088,32 @@ func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.Prepare } } -func applyTextDocumentEdits(r *runner, edits []protocol.TextDocumentEdit) (map[span.URI]string, error) { +func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[span.URI]string, error) { res := map[span.URI]string{} for _, docEdits := range edits { - uri := docEdits.TextDocument.URI.SpanURI() - var m *protocol.ColumnMapper - // If we have already edited this file, we use the edited version (rather than the - // file in its original state) so that we preserve our initial changes. - if content, ok := res[uri]; ok { - m = protocol.NewColumnMapper(uri, []byte(content)) - } else { - var err error - if m, err = r.data.Mapper(uri); err != nil { + if docEdits.TextDocumentEdit != nil { + uri := docEdits.TextDocumentEdit.TextDocument.URI.SpanURI() + var m *protocol.ColumnMapper + // If we have already edited this file, we use the edited version (rather than the + // file in its original state) so that we preserve our initial changes. + if content, ok := res[uri]; ok { + m = protocol.NewColumnMapper(uri, []byte(content)) + } else { + var err error + if m, err = r.data.Mapper(uri); err != nil { + return nil, err + } + } + patched, _, err := source.ApplyProtocolEdits(m, docEdits.TextDocumentEdit.Edits) + if err != nil { return nil, err } + res[uri] = patched } - res[uri] = string(m.Content) - sedits, err := source.FromProtocolEdits(m, docEdits.Edits) - if err != nil { - return nil, err - } - res[uri] = applyEdits(res[uri], sedits) } return res, nil } -func applyEdits(contents string, edits []diff.TextEdit) string { - res := contents - - // Apply the edits from the end of the file forward - // to preserve the offsets - for i := len(edits) - 1; i >= 0; i-- { - edit := edits[i] - start := edit.Span.Start().Offset() - end := edit.Span.End().Offset() - tmp := res[0:start] + edit.NewText - res = tmp + res[end:] - } - return res -} - func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { params := &protocol.DocumentSymbolParams{ TextDocument: protocol.TextDocumentIdentifier{ @@ -1086,10 +1124,7 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol. if err != nil { t.Fatal(err) } - if len(got) != len(expectedSymbols) { - t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(got)) - return - } + symbols := make([]protocol.DocumentSymbol, len(got)) for i, s := range got { s, ok := s.(protocol.DocumentSymbol) @@ -1098,18 +1133,25 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol. } symbols[i] = s } - if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" { - t.Error(diff) + + // Sort by position to make it easier to find errors. + sortSymbols := func(s []protocol.DocumentSymbol) { + sort.Slice(s, func(i, j int) bool { + return protocol.CompareRange(s[i].SelectionRange, s[j].SelectionRange) < 0 + }) } -} + sortSymbols(expectedSymbols) + sortSymbols(symbols) -func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - r.callWorkspaceSymbols(t, uri, query, typ) + // Ignore 'Range' here as it is difficult (impossible?) to express + // multi-line ranges in the packagestest framework. + ignoreRange := cmpopts.IgnoreFields(protocol.DocumentSymbol{}, "Range") + if diff := cmp.Diff(expectedSymbols, symbols, ignoreRange); diff != "" { + t.Errorf("mismatching symbols (-want +got)\n%s", diff) + } } -func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - t.Helper() - +func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ) original := r.server.session.Options() @@ -1130,10 +1172,10 @@ func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, t.Fatal(err) } got = filepath.ToSlash(tests.Normalize(got, r.normalizers)) - want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) - if diff := tests.Diff(t, want, got); diff != "" { + if diff := compare.Text(want, got); diff != "" { t.Error(diff) } } @@ -1173,11 +1215,7 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa if got == nil { t.Fatalf("expected %v, got nil", want) } - diff, err := tests.DiffSignatures(spn, want, got) - if err != nil { - t.Fatal(err) - } - if diff != "" { + if diff := tests.DiffSignatures(spn, want, got); diff != "" { t.Error(diff) } } @@ -1240,13 +1278,13 @@ func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) { t.Fatal(err) } got := (<-r.editRecv)[uri] - want := r.data.Golden("addimport", uri.Filename(), func() ([]byte, error) { + want := r.data.Golden(t, "addimport", uri.Filename(), func() ([]byte, error) { return []byte(got), nil }) if want == nil { t.Fatalf("golden file %q not found", uri.Filename()) } - if diff := tests.Diff(t, got, string(want)); diff != "" { + if diff := compare.Text(got, string(want)); diff != "" { t.Errorf("%s mismatch\n%s", command.AddImport, diff) } } diff --git a/internal/lsp/lsppos/lsppos.go b/gopls/internal/lsp/lsppos/lsppos.go similarity index 93% rename from internal/lsp/lsppos/lsppos.go rename to gopls/internal/lsp/lsppos/lsppos.go index 35f6f134854..425a4f74aeb 100644 --- a/internal/lsp/lsppos/lsppos.go +++ b/gopls/internal/lsp/lsppos/lsppos.go @@ -17,11 +17,12 @@ package lsppos import ( + "bytes" "errors" "sort" "unicode/utf8" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // Mapper maps utf-8 byte offsets to LSP positions for a single file. @@ -36,9 +37,10 @@ type Mapper struct { // NewMapper creates a new Mapper for the given content. func NewMapper(content []byte) *Mapper { + nlines := bytes.Count(content, []byte("\n")) m := &Mapper{ content: content, - lines: []int{0}, + lines: make([]int, 1, nlines+1), // initially []int{0} } for offset, b := range content { if b == '\n' { @@ -116,7 +118,7 @@ func (m *Mapper) Range(start, end int) (protocol.Range, error) { return protocol.Range{Start: startPos, End: endPos}, nil } -// UTF16Len returns the UTF-16 length of the UTF-8 encoded content, were it to +// UTF16len returns the UTF-16 length of the UTF-8 encoded content, were it to // be re-encoded as UTF-16. func UTF16len(buf []byte) int { // This function copies buf, but microbenchmarks showed it to be faster than diff --git a/internal/lsp/lsppos/lsppos_test.go b/gopls/internal/lsp/lsppos/lsppos_test.go similarity index 96% rename from internal/lsp/lsppos/lsppos_test.go rename to gopls/internal/lsp/lsppos/lsppos_test.go index 8353f927681..f65b64ff804 100644 --- a/internal/lsp/lsppos/lsppos_test.go +++ b/gopls/internal/lsp/lsppos/lsppos_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) type testCase struct { diff --git a/internal/lsp/lsppos/token.go b/gopls/internal/lsp/lsppos/token.go similarity index 81% rename from internal/lsp/lsppos/token.go rename to gopls/internal/lsp/lsppos/token.go index 0f1f2b24c7b..a42b5dad331 100644 --- a/internal/lsp/lsppos/token.go +++ b/gopls/internal/lsp/lsppos/token.go @@ -6,10 +6,11 @@ package lsppos import ( "errors" + "go/ast" "go/token" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" ) // TokenMapper maps token.Pos to LSP positions for a single file. @@ -24,7 +25,7 @@ type TokenMapper struct { mapper *Mapper } -// NewMapper creates a new TokenMapper for the given content, using the +// NewTokenMapper creates a new TokenMapper for the given content, using the // provided file to compute offsets. func NewTokenMapper(content []byte, file *token.File) *TokenMapper { return &TokenMapper{ @@ -58,3 +59,9 @@ func (m *TokenMapper) Range(start, end token.Pos) (protocol.Range, error) { return protocol.Range{Start: startPos, End: endPos}, nil } + +// NodeRange returns the protocol range corresponding to the span of the given +// node. +func (m *TokenMapper) NodeRange(n ast.Node) (protocol.Range, error) { + return m.Range(n.Pos(), n.End()) +} diff --git a/internal/lsp/lsppos/token_test.go b/gopls/internal/lsp/lsppos/token_test.go similarity index 93% rename from internal/lsp/lsppos/token_test.go rename to gopls/internal/lsp/lsppos/token_test.go index c12d15026c7..a8fa6f667c7 100644 --- a/internal/lsp/lsppos/token_test.go +++ b/gopls/internal/lsp/lsppos/token_test.go @@ -8,8 +8,8 @@ import ( "go/token" "testing" - . "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func makeTokenMapper(content []byte) (*TokenMapper, *token.File) { diff --git a/internal/lsp/lsprpc/autostart_default.go b/gopls/internal/lsp/lsprpc/autostart_default.go similarity index 92% rename from internal/lsp/lsprpc/autostart_default.go rename to gopls/internal/lsp/lsprpc/autostart_default.go index 59a76dc2f9f..20b974728d9 100644 --- a/internal/lsp/lsprpc/autostart_default.go +++ b/gopls/internal/lsp/lsprpc/autostart_default.go @@ -24,7 +24,7 @@ func runRemote(cmd *exec.Cmd) error { return nil } -// autoNetworkAddress returns the default network and address for the +// autoNetworkAddressDefault returns the default network and address for the // automatically-started gopls remote. See autostart_posix.go for more // information. func autoNetworkAddressDefault(goplsPath, id string) (network string, address string) { diff --git a/internal/lsp/lsprpc/autostart_posix.go b/gopls/internal/lsp/lsprpc/autostart_posix.go similarity index 97% rename from internal/lsp/lsprpc/autostart_posix.go rename to gopls/internal/lsp/lsprpc/autostart_posix.go index 948d44fcedf..90cc72ddf10 100644 --- a/internal/lsp/lsprpc/autostart_posix.go +++ b/gopls/internal/lsp/lsprpc/autostart_posix.go @@ -33,7 +33,7 @@ func daemonizePosix(cmd *exec.Cmd) { } } -// autoNetworkAddress resolves an id on the 'auto' pseduo-network to a +// autoNetworkAddressPosix resolves an id on the 'auto' pseduo-network to a // real network and address. On unix, this uses unix domain sockets. func autoNetworkAddressPosix(goplsPath, id string) (network string, address string) { // Especially when doing local development or testing, it's important that diff --git a/internal/lsp/lsprpc/binder.go b/gopls/internal/lsp/lsprpc/binder.go similarity index 88% rename from internal/lsp/lsprpc/binder.go rename to gopls/internal/lsp/lsprpc/binder.go index aa2edb3309d..01e59f7bb62 100644 --- a/internal/lsp/lsprpc/binder.go +++ b/gopls/internal/lsp/lsprpc/binder.go @@ -9,17 +9,17 @@ import ( "encoding/json" "fmt" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/xcontext" ) // The BinderFunc type adapts a bind function to implement the jsonrpc2.Binder // interface. -type BinderFunc func(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) +type BinderFunc func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions -func (f BinderFunc) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { +func (f BinderFunc) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { return f(ctx, conn) } @@ -39,7 +39,7 @@ func NewServerBinder(newServer ServerFunc) *ServerBinder { return &ServerBinder{newServer: newServer} } -func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { +func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { client := protocol.ClientDispatcherV2(conn) server := b.newServer(ctx, client) serverHandler := protocol.ServerHandlerV2(server) @@ -55,7 +55,7 @@ func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) ( return jsonrpc2_v2.ConnectionOptions{ Handler: wrapped, Preempter: preempter, - }, nil + } } type canceler struct { @@ -94,13 +94,19 @@ func NewForwardBinder(dialer jsonrpc2_v2.Dialer) *ForwardBinder { } } -func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (opts jsonrpc2_v2.ConnectionOptions, _ error) { +func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (opts jsonrpc2_v2.ConnectionOptions) { client := protocol.ClientDispatcherV2(conn) clientBinder := NewClientBinder(func(context.Context, protocol.Server) protocol.Client { return client }) + serverConn, err := jsonrpc2_v2.Dial(context.Background(), b.dialer, clientBinder) if err != nil { - return opts, err + return jsonrpc2_v2.ConnectionOptions{ + Handler: jsonrpc2_v2.HandlerFunc(func(context.Context, *jsonrpc2_v2.Request) (interface{}, error) { + return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrInternal, err) + }), + } } + if b.onBind != nil { b.onBind(serverConn) } @@ -118,7 +124,7 @@ func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) return jsonrpc2_v2.ConnectionOptions{ Handler: protocol.ServerHandlerV2(server), Preempter: preempter, - }, nil + } } // A ClientFunc is used to construct an LSP client for a given server. @@ -133,10 +139,10 @@ func NewClientBinder(newClient ClientFunc) *ClientBinder { return &ClientBinder{newClient} } -func (b *ClientBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { +func (b *ClientBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { server := protocol.ServerDispatcherV2(conn) client := b.newClient(ctx, server) return jsonrpc2_v2.ConnectionOptions{ Handler: protocol.ClientHandlerV2(client), - }, nil + } } diff --git a/internal/lsp/lsprpc/binder_test.go b/gopls/internal/lsp/lsprpc/binder_test.go similarity index 90% rename from internal/lsp/lsprpc/binder_test.go rename to gopls/internal/lsp/lsprpc/binder_test.go index f7dd830331c..3315c3eb775 100644 --- a/internal/lsp/lsprpc/binder_test.go +++ b/gopls/internal/lsp/lsprpc/binder_test.go @@ -11,23 +11,20 @@ import ( "testing" "time" + "golang.org/x/tools/gopls/internal/lsp/protocol" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/lsprpc" + . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) type TestEnv struct { - Listeners []jsonrpc2_v2.Listener - Conns []*jsonrpc2_v2.Connection - Servers []*jsonrpc2_v2.Server + Conns []*jsonrpc2_v2.Connection + Servers []*jsonrpc2_v2.Server } func (e *TestEnv) Shutdown(t *testing.T) { - for _, l := range e.Listeners { - if err := l.Close(); err != nil { - t.Error(err) - } + for _, s := range e.Servers { + s.Shutdown() } for _, c := range e.Conns { if err := c.Close(); err != nil { @@ -46,11 +43,7 @@ func (e *TestEnv) serve(ctx context.Context, t *testing.T, server jsonrpc2_v2.Bi if err != nil { t.Fatal(err) } - e.Listeners = append(e.Listeners, l) - s, err := jsonrpc2_v2.Serve(ctx, l, server) - if err != nil { - t.Fatal(err) - } + s := jsonrpc2_v2.NewServer(ctx, l, server) e.Servers = append(e.Servers, s) return l, s } diff --git a/internal/lsp/lsprpc/commandinterceptor.go b/gopls/internal/lsp/lsprpc/commandinterceptor.go similarity index 86% rename from internal/lsp/lsprpc/commandinterceptor.go rename to gopls/internal/lsp/lsprpc/commandinterceptor.go index 5c36af759e1..607ee9c9e9f 100644 --- a/internal/lsp/lsprpc/commandinterceptor.go +++ b/gopls/internal/lsp/lsprpc/commandinterceptor.go @@ -8,8 +8,8 @@ import ( "context" "encoding/json" + "golang.org/x/tools/gopls/internal/lsp/protocol" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/lsp/protocol" ) // HandlerMiddleware is a middleware that only modifies the jsonrpc2 handler. @@ -18,13 +18,10 @@ type HandlerMiddleware func(jsonrpc2_v2.Handler) jsonrpc2_v2.Handler // BindHandler transforms a HandlerMiddleware into a Middleware. func BindHandler(hmw HandlerMiddleware) Middleware { return Middleware(func(binder jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { - return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { - opts, err := binder.Bind(ctx, conn) - if err != nil { - return opts, err - } + return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { + opts := binder.Bind(ctx, conn) opts.Handler = hmw(opts.Handler) - return opts, nil + return opts }) }) } diff --git a/internal/lsp/lsprpc/commandinterceptor_test.go b/gopls/internal/lsp/lsprpc/commandinterceptor_test.go similarity index 90% rename from internal/lsp/lsprpc/commandinterceptor_test.go rename to gopls/internal/lsp/lsprpc/commandinterceptor_test.go index 06550e8fa7d..555f15130cc 100644 --- a/internal/lsp/lsprpc/commandinterceptor_test.go +++ b/gopls/internal/lsp/lsprpc/commandinterceptor_test.go @@ -8,9 +8,9 @@ import ( "context" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/lsprpc" + . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) func TestCommandInterceptor(t *testing.T) { diff --git a/internal/lsp/lsprpc/dialer.go b/gopls/internal/lsp/lsprpc/dialer.go similarity index 100% rename from internal/lsp/lsprpc/dialer.go rename to gopls/internal/lsp/lsprpc/dialer.go diff --git a/internal/lsp/lsprpc/goenv.go b/gopls/internal/lsp/lsprpc/goenv.go similarity index 98% rename from internal/lsp/lsprpc/goenv.go rename to gopls/internal/lsp/lsprpc/goenv.go index f313724c875..c316ea07c70 100644 --- a/internal/lsp/lsprpc/goenv.go +++ b/gopls/internal/lsp/lsprpc/goenv.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func GoEnvMiddleware() (Middleware, error) { diff --git a/internal/lsp/lsprpc/goenv_test.go b/gopls/internal/lsp/lsprpc/goenv_test.go similarity index 94% rename from internal/lsp/lsprpc/goenv_test.go rename to gopls/internal/lsp/lsprpc/goenv_test.go index cdfe23c9089..b4a1b0ddaf5 100644 --- a/internal/lsp/lsprpc/goenv_test.go +++ b/gopls/internal/lsp/lsprpc/goenv_test.go @@ -8,10 +8,10 @@ import ( "context" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" - . "golang.org/x/tools/internal/lsp/lsprpc" + . "golang.org/x/tools/gopls/internal/lsp/lsprpc" ) type initServer struct { diff --git a/internal/lsp/lsprpc/lsprpc.go b/gopls/internal/lsp/lsprpc/lsprpc.go similarity index 97% rename from internal/lsp/lsprpc/lsprpc.go rename to gopls/internal/lsp/lsprpc/lsprpc.go index a85e7914219..f0fe53dcf59 100644 --- a/internal/lsp/lsprpc/lsprpc.go +++ b/gopls/internal/lsp/lsprpc/lsprpc.go @@ -21,12 +21,12 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // Unique identifiers for client/server. @@ -56,7 +56,9 @@ func (s *StreamServer) Binder() *ServerBinder { server := s.serverForTest if server == nil { server = lsp.NewServer(session, client) - debug.GetInstance(ctx).AddService(server, session) + if instance := debug.GetInstance(ctx); instance != nil { + instance.AddService(server, session) + } } return server } @@ -71,7 +73,9 @@ func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) erro server := s.serverForTest if server == nil { server = lsp.NewServer(session, client) - debug.GetInstance(ctx).AddService(server, session) + if instance := debug.GetInstance(ctx); instance != nil { + instance.AddService(server, session) + } } // Clients may or may not send a shutdown message. Make sure the server is // shut down. diff --git a/internal/lsp/lsprpc/lsprpc_test.go b/gopls/internal/lsp/lsprpc/lsprpc_test.go similarity index 93% rename from internal/lsp/lsprpc/lsprpc_test.go rename to gopls/internal/lsp/lsprpc/lsprpc_test.go index 795c887e4b4..5718dfec1fa 100644 --- a/internal/lsp/lsprpc/lsprpc_test.go +++ b/gopls/internal/lsp/lsprpc/lsprpc_test.go @@ -15,10 +15,10 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -58,9 +58,9 @@ func TestClientLogging(t *testing.T) { client := FakeClient{Logs: make(chan string, 10)} ctx = debug.WithInstance(ctx, "", "") - ss := NewStreamServer(cache.New(nil), false) + ss := NewStreamServer(cache.New(nil, nil, nil), false) ss.serverForTest = server - ts := servertest.NewPipeServer(ctx, ss, nil) + ts := servertest.NewPipeServer(ss, nil) defer checkClose(t, ts.Close) cc := ts.Connect(ctx) cc.Go(ctx, protocol.ClientHandler(client, jsonrpc2.MethodNotFound)) @@ -121,16 +121,15 @@ func checkClose(t *testing.T, closer func() error) { func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) { t.Helper() serveCtx := debug.WithInstance(ctx, "", "") - ss := NewStreamServer(cache.New(nil), false) + ss := NewStreamServer(cache.New(nil, nil, nil), false) ss.serverForTest = s tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) - forwarderCtx := debug.WithInstance(ctx, "", "") forwarder, err := NewForwarder("tcp;"+tsDirect.Addr, nil) if err != nil { t.Fatal(err) } - tsForwarded := servertest.NewPipeServer(forwarderCtx, forwarder, nil) + tsForwarded := servertest.NewPipeServer(forwarder, nil) return tsDirect, tsForwarded, func() { checkClose(t, tsDirect.Close) checkClose(t, tsForwarded.Close) @@ -217,7 +216,7 @@ func TestDebugInfoLifecycle(t *testing.T) { clientCtx := debug.WithInstance(baseCtx, "", "") serverCtx := debug.WithInstance(baseCtx, "", "") - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) ss := NewStreamServer(cache, false) tsBackend := servertest.NewTCPServer(serverCtx, ss, nil) @@ -225,16 +224,14 @@ func TestDebugInfoLifecycle(t *testing.T) { if err != nil { t.Fatal(err) } - tsForwarder := servertest.NewPipeServer(clientCtx, forwarder, nil) + tsForwarder := servertest.NewPipeServer(forwarder, nil) - conn1 := tsForwarder.Connect(clientCtx) - ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, conn1, fake.ClientHooks{}) + ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, tsForwarder, fake.ClientHooks{}) if err != nil { t.Fatal(err) } defer ed1.Close(clientCtx) - conn2 := tsBackend.Connect(baseCtx) - ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, conn2, fake.ClientHooks{}) + ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, tsBackend, fake.ClientHooks{}) if err != nil { t.Fatal(err) } diff --git a/internal/lsp/lsprpc/middleware.go b/gopls/internal/lsp/lsprpc/middleware.go similarity index 96% rename from internal/lsp/lsprpc/middleware.go rename to gopls/internal/lsp/lsprpc/middleware.go index f703217dd0b..50089cde7dc 100644 --- a/internal/lsp/lsprpc/middleware.go +++ b/gopls/internal/lsp/lsprpc/middleware.go @@ -62,11 +62,8 @@ func (h *Handshaker) Peers() []PeerInfo { // Middleware is a jsonrpc2 middleware function to augment connection binding // to handle the handshake method, and record disconnections. func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { - return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { - opts, err := inner.Bind(ctx, conn) - if err != nil { - return opts, err - } + return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { + opts := inner.Bind(ctx, conn) localID := h.nextID() info := &PeerInfo{ @@ -93,7 +90,7 @@ func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { // Record the dropped client. go h.cleanupAtDisconnect(conn, localID) - return opts, nil + return opts }) } diff --git a/internal/lsp/lsprpc/middleware_test.go b/gopls/internal/lsp/lsprpc/middleware_test.go similarity index 93% rename from internal/lsp/lsprpc/middleware_test.go rename to gopls/internal/lsp/lsprpc/middleware_test.go index a385f10037a..c528eae5c62 100644 --- a/internal/lsp/lsprpc/middleware_test.go +++ b/gopls/internal/lsp/lsprpc/middleware_test.go @@ -11,12 +11,12 @@ import ( "testing" "time" + . "golang.org/x/tools/gopls/internal/lsp/lsprpc" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - . "golang.org/x/tools/internal/lsp/lsprpc" ) -var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) (jsonrpc2_v2.ConnectionOptions, error) { - return jsonrpc2_v2.ConnectionOptions{}, nil +var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { + return jsonrpc2_v2.ConnectionOptions{} }) func TestHandshakeMiddleware(t *testing.T) { diff --git a/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go similarity index 78% rename from internal/lsp/mod/code_lens.go rename to gopls/internal/lsp/mod/code_lens.go index b26bae75c47..01d75d92a20 100644 --- a/internal/lsp/mod/code_lens.go +++ b/gopls/internal/lsp/mod/code_lens.go @@ -11,9 +11,9 @@ import ( "path/filepath" "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) // LensFuncs returns the supported lensFuncs for go.mod files. @@ -22,6 +22,7 @@ func LensFuncs() map[command.Command]source.LensFunc { command.UpgradeDependency: upgradeLenses, command.Tidy: tidyLens, command.Vendor: vendorLens, + command.RunVulncheckExp: vulncheckLenses, } } @@ -62,6 +63,10 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File if err != nil { return nil, err } + reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.URIArg{URI: uri}) + if err != nil { + return nil, err + } // Put the upgrade code lenses above the first require block or statement. rng, err := firstRequireRange(fh, pm) if err != nil { @@ -72,6 +77,7 @@ func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.File {Range: rng, Command: checkUpgrade}, {Range: rng, Command: upgradeTransitive}, {Range: rng, Command: upgradeDirect}, + {Range: rng, Command: reset}, }, nil } @@ -128,7 +134,7 @@ func moduleStmtRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Ra return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI()) } syntax := pm.File.Module.Syntax - return source.LineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End) + return pm.Mapper.OffsetRange(syntax.Start.Byte, syntax.End.Byte) } // firstRequireRange returns the range for the first "require" in the given @@ -149,5 +155,31 @@ func firstRequireRange(fh source.FileHandle, pm *source.ParsedModule) (protocol. if start.Byte == 0 || firstRequire.Start.Byte < start.Byte { start, end = firstRequire.Start, firstRequire.End } - return source.LineToRange(pm.Mapper, fh.URI(), start, end) + return pm.Mapper.OffsetRange(start.Byte, end.Byte) +} + +func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil || pm.File == nil { + return nil, err + } + // Place the codelenses near the module statement. + // A module may not have the require block, + // but vulnerabilities can exist in standard libraries. + uri := protocol.URIFromSpanURI(fh.URI()) + rng, err := moduleStmtRange(fh, pm) + if err != nil { + return nil, err + } + + vulncheck, err := command.NewRunVulncheckExpCommand("Run govulncheck", command.VulncheckArgs{ + URI: uri, + Pattern: "./...", + }) + if err != nil { + return nil, err + } + return []protocol.CodeLens{ + {Range: rng, Command: vulncheck}, + }, nil } diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go new file mode 100644 index 00000000000..770fb3729f7 --- /dev/null +++ b/gopls/internal/lsp/mod/diagnostics.go @@ -0,0 +1,371 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package mod provides core features related to go.mod file +// handling for use by Go editors and tools. +package mod + +import ( + "context" + "fmt" + "sort" + "strings" + + "golang.org/x/mod/modfile" + "golang.org/x/mod/semver" + "golang.org/x/tools/gopls/internal/govulncheck" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/vuln/osv" +) + +// Diagnostics returns diagnostics for the modules in the workspace. +// +// It waits for completion of type-checking of all active packages. +func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { + ctx, done := event.Start(ctx, "mod.Diagnostics", tag.Snapshot.Of(snapshot.ID())) + defer done() + + return collectDiagnostics(ctx, snapshot, ModDiagnostics) +} + +// UpgradeDiagnostics returns upgrade diagnostics for the modules in the +// workspace with known upgrades. +func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { + ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", tag.Snapshot.Of(snapshot.ID())) + defer done() + + return collectDiagnostics(ctx, snapshot, ModUpgradeDiagnostics) +} + +// VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the +// workspace with known vulnerabilites. +func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { + ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", tag.Snapshot.Of(snapshot.ID())) + defer done() + + return collectDiagnostics(ctx, snapshot, ModVulnerabilityDiagnostics) +} + +func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { + reports := make(map[source.VersionedFileIdentity][]*source.Diagnostic) + for _, uri := range snapshot.ModFiles() { + fh, err := snapshot.GetVersionedFile(ctx, uri) + if err != nil { + return nil, err + } + reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{} + diagnostics, err := diagFn(ctx, snapshot, fh) + if err != nil { + return nil, err + } + for _, d := range diagnostics { + fh, err := snapshot.GetVersionedFile(ctx, d.URI) + if err != nil { + return nil, err + } + reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], d) + } + } + return reports, nil +} + +// ModDiagnostics waits for completion of type-checking of all active +// packages, then returns diagnostics from diagnosing the packages in +// the workspace and from tidying the go.mod file. +func ModDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (diagnostics []*source.Diagnostic, err error) { + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil { + if pm == nil || len(pm.ParseErrors) == 0 { + return nil, err + } + return pm.ParseErrors, nil + } + + // Packages in the workspace can contribute diagnostics to go.mod files. + // TODO(rfindley): Try to avoid calling DiagnosePackage on all packages in the workspace here, + // for every go.mod file. If gc_details is enabled, it looks like this could lead to extra + // go command invocations (as gc details is not memoized). + wspkgs, err := snapshot.ActivePackages(ctx) + if err != nil && !source.IsNonFatalGoModError(err) { + event.Error(ctx, fmt.Sprintf("workspace packages: diagnosing %s", pm.URI), err) + } + if err == nil { + for _, pkg := range wspkgs { + pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) + if err != nil { + return nil, err + } + diagnostics = append(diagnostics, pkgDiagnostics[fh.URI()]...) + } + } + + tidied, err := snapshot.ModTidy(ctx, pm) + if err != nil && !source.IsNonFatalGoModError(err) { + event.Error(ctx, fmt.Sprintf("tidy: diagnosing %s", pm.URI), err) + } + if err == nil { + for _, d := range tidied.Diagnostics { + if d.URI != fh.URI() { + continue + } + diagnostics = append(diagnostics, d) + } + } + return diagnostics, nil +} + +// ModUpgradeDiagnostics adds upgrade quick fixes for individual modules if the upgrades +// are recorded in the view. +func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (upgradeDiagnostics []*source.Diagnostic, err error) { + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil { + // Don't return an error if there are parse error diagnostics to be shown, but also do not + // continue since we won't be able to show the upgrade diagnostics. + if pm != nil && len(pm.ParseErrors) != 0 { + return nil, nil + } + return nil, err + } + + upgrades := snapshot.View().ModuleUpgrades(fh.URI()) + for _, req := range pm.File.Require { + ver, ok := upgrades[req.Mod.Path] + if !ok || req.Mod.Version == ver { + continue + } + rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) + if err != nil { + return nil, err + } + // Upgrade to the exact version we offer the user, not the most recent. + title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, ver) + cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ + URI: protocol.URIFromSpanURI(fh.URI()), + AddRequire: false, + GoCmdArgs: []string{req.Mod.Path + "@" + ver}, + }) + if err != nil { + return nil, err + } + upgradeDiagnostics = append(upgradeDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: protocol.SeverityInformation, + Source: source.UpgradeNotification, + Message: fmt.Sprintf("%v can be upgraded", req.Mod.Path), + SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, + }) + } + + return upgradeDiagnostics, nil +} + +func pkgVersion(pkgVersion string) (pkg, ver string) { + if pkgVersion == "" { + return "", "" + } + at := strings.Index(pkgVersion, "@") + switch { + case at < 0: + return pkgVersion, "" + case at == 0: + return "", pkgVersion[1:] + default: + return pkgVersion[:at], pkgVersion[at+1:] + } +} + +const upgradeCodeActionPrefix = "Upgrade to " + +// ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules +// if the vulnerability is recorded in the view. +func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (vulnDiagnostics []*source.Diagnostic, err error) { + pm, err := snapshot.ParseMod(ctx, fh) + if err != nil { + // Don't return an error if there are parse error diagnostics to be shown, but also do not + // continue since we won't be able to show the vulnerability diagnostics. + if pm != nil && len(pm.ParseErrors) != 0 { + return nil, nil + } + return nil, err + } + + vs := snapshot.View().Vulnerabilities(fh.URI()) + // TODO(suzmue): should we just store the vulnerabilities like this? + affecting := make(map[string][]govulncheck.Vuln) + nonaffecting := make(map[string][]govulncheck.Vuln) + for _, v := range vs { + if len(v.Trace) > 0 { + affecting[v.ModPath] = append(affecting[v.ModPath], v) + } else { + nonaffecting[v.ModPath] = append(nonaffecting[v.ModPath], v) + } + } + + for _, req := range pm.File.Require { + affectingVulns, ok := affecting[req.Mod.Path] + nonaffectingVulns, ok2 := nonaffecting[req.Mod.Path] + if !ok && !ok2 { + continue + } + rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) + if err != nil { + return nil, err + } + // Map affecting vulns to 'warning' level diagnostics, + // others to 'info' level diagnostics. + // Fixes will include only the upgrades for warning level diagnostics. + var fixes []source.SuggestedFix + var warning, info []string + for _, v := range nonaffectingVulns { + // Only show the diagnostic if the vulnerability was calculated + // for the module at the current version. + if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 { + continue + } + info = append(info, v.OSV.ID) + } + for _, v := range affectingVulns { + // Only show the diagnostic if the vulnerability was calculated + // for the module at the current version. + if semver.IsValid(v.FoundIn) && semver.Compare(req.Mod.Version, v.FoundIn) != 0 { + continue + } + warning = append(warning, v.OSV.ID) + // Upgrade to the exact version we offer the user, not the most recent. + // TODO(hakim): Produce fixes only for affecting vulnerabilities (if len(v.Trace) > 0) + + if fixedVersion := v.FixedIn; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { + cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) + if err != nil { + return nil, err + } + // Add an upgrade for module@latest. + // TODO(suzmue): verify if latest is the same as fixedVersion. + latest, err := getUpgradeCodeAction(fh, req, "latest") + if err != nil { + return nil, err + } + + fixes = []source.SuggestedFix{ + source.SuggestedFixFromCommand(cmd, protocol.QuickFix), + source.SuggestedFixFromCommand(latest, protocol.QuickFix), + } + } + } + + if len(warning) == 0 && len(info) == 0 { + return nil, nil + } + severity := protocol.SeverityInformation + if len(warning) > 0 { + severity = protocol.SeverityWarning + } + + sort.Strings(warning) + sort.Strings(info) + + var b strings.Builder + if len(warning) == 1 { + fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0]) + } else { + fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", ")) + } + if len(warning) == 0 { + if len(info) == 1 { + fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", req.Mod.Path, info[0]) + } else { + fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", req.Mod.Path, strings.Join(info, ", ")) + } + } + + vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ + URI: fh.URI(), + Range: rng, + Severity: severity, + Source: source.Vulncheck, + Message: b.String(), + SuggestedFixes: fixes, + }) + } + + return vulnDiagnostics, nil +} + +func formatMessage(v govulncheck.Vuln) string { + details := []byte(v.OSV.Details) + // Remove any new lines that are not preceded or followed by a new line. + for i, r := range details { + if r == '\n' && i > 0 && details[i-1] != '\n' && i+1 < len(details) && details[i+1] != '\n' { + details[i] = ' ' + } + } + return strings.TrimSpace(strings.Replace(string(details), "\n\n", "\n\n ", -1)) +} + +// href returns a URL embedded in the entry if any. +// If no suitable URL is found, it returns a default entry in +// pkg.go.dev/vuln. +func href(vuln *osv.Entry) string { + for _, affected := range vuln.Affected { + if url := affected.DatabaseSpecific.URL; url != "" { + return url + } + } + for _, r := range vuln.References { + if r.Type == "WEB" { + return r.URL + } + } + return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vuln.ID) +} + +func getUpgradeCodeAction(fh source.FileHandle, req *modfile.Require, version string) (protocol.Command, error) { + cmd, err := command.NewUpgradeDependencyCommand(upgradeTitle(version), command.DependencyArgs{ + URI: protocol.URIFromSpanURI(fh.URI()), + AddRequire: false, + GoCmdArgs: []string{req.Mod.Path + "@" + version}, + }) + if err != nil { + return protocol.Command{}, err + } + return cmd, nil +} + +func upgradeTitle(fixedVersion string) string { + title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, fixedVersion) + return title +} + +// SelectUpgradeCodeActions takes a list of upgrade code actions for a +// required module and returns a more selective list of upgrade code actions, +// where the code actions have been deduped. +func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeAction { + // TODO(suzmue): we can further limit the code actions to only return the most + // recent version that will fix all the vulnerabilities. + + set := make(map[string]protocol.CodeAction) + for _, action := range actions { + set[action.Command.Title] = action + } + var result []protocol.CodeAction + for _, action := range set { + result = append(result, action) + } + // Sort results by version number, latest first. + // There should be no duplicates at this point. + sort.Slice(result, func(i, j int) bool { + vi, vj := getUpgradeVersion(result[i]), getUpgradeVersion(result[j]) + return vi == "latest" || (vj != "latest" && semver.Compare(vi, vj) > 0) + }) + return result +} + +func getUpgradeVersion(p protocol.CodeAction) string { + return strings.TrimPrefix(p.Title, upgradeCodeActionPrefix) +} diff --git a/internal/lsp/mod/format.go b/gopls/internal/lsp/mod/format.go similarity index 68% rename from internal/lsp/mod/format.go rename to gopls/internal/lsp/mod/format.go index c3557663272..9c3942ee06d 100644 --- a/internal/lsp/mod/format.go +++ b/gopls/internal/lsp/mod/format.go @@ -7,9 +7,9 @@ package mod import ( "context" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" ) func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) { @@ -25,9 +25,6 @@ func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) return nil, err } // Calculate the edits to be made due to the change. - diff, err := snapshot.View().Options().ComputeEdits(fh.URI(), string(pm.Mapper.Content), string(formatted)) - if err != nil { - return nil, err - } - return source.ToProtocolEdits(pm.Mapper, diff) + diffs := snapshot.View().Options().ComputeEdits(string(pm.Mapper.Content), string(formatted)) + return source.ToProtocolEdits(pm.Mapper, diffs) } diff --git a/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go similarity index 54% rename from internal/lsp/mod/hover.go rename to gopls/internal/lsp/mod/hover.go index 1461d52edbd..5d5b6158212 100644 --- a/internal/lsp/mod/hover.go +++ b/gopls/internal/lsp/mod/hover.go @@ -8,12 +8,14 @@ import ( "bytes" "context" "fmt" + "sort" "strings" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/govulncheck" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" ) func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { @@ -55,18 +57,22 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Shift the start position to the location of the // dependency within the require statement. - startPos, endPos = s+i, s+i+len(dep) + startPos, endPos = s+i, e if startPos <= offset && offset <= endPos { req = r break } } + // TODO(hyangah): find position for info about vulnerabilities in Go // The cursor position is not on a require statement. if req == nil { return nil, nil } + // Get the vulnerability info. + affecting, nonaffecting := lookupVulns(snapshot.View().Vulnerabilities(fh.URI()), req) + // Get the `go mod why` results for the given file. why, err := snapshot.ModWhy(ctx, fh) if err != nil { @@ -78,38 +84,120 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, } // Get the range to highlight for the hover. - rng, err := source.ByteOffsetsToRange(pm.Mapper, fh.URI(), startPos, endPos) - if err != nil { - return nil, err - } + // TODO(hyangah): adjust the hover range to include the version number + // to match the diagnostics' range. + rng, err := pm.Mapper.OffsetRange(startPos, endPos) if err != nil { return nil, err } options := snapshot.View().Options() isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path) + header := formatHeader(req.Mod.Path, options) explanation = formatExplanation(explanation, req, options, isPrivate) + vulns := formatVulnerabilities(affecting, nonaffecting, options) + return &protocol.Hover{ Contents: protocol.MarkupContent{ Kind: options.PreferredContentFormat, - Value: explanation, + Value: header + vulns + explanation, }, Range: rng, }, nil } -func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string { - text = strings.TrimSuffix(text, "\n") - splt := strings.Split(text, "\n") - length := len(splt) - +func formatHeader(modpath string, options *source.Options) string { var b strings.Builder // Write the heading as an H3. - b.WriteString("##" + splt[0]) + b.WriteString("#### " + modpath) if options.PreferredContentFormat == protocol.Markdown { b.WriteString("\n\n") } else { b.WriteRune('\n') } + return b.String() +} + +func compareVuln(i, j govulncheck.Vuln) bool { + if i.OSV.ID == j.OSV.ID { + return i.PkgPath < j.PkgPath + } + return i.OSV.ID < j.OSV.ID +} + +func lookupVulns(vulns []govulncheck.Vuln, req *modfile.Require) (affecting, nonaffecting []govulncheck.Vuln) { + modpath, modversion := req.Mod.Path, req.Mod.Version + + var info, warning []govulncheck.Vuln + for _, vuln := range vulns { + if vuln.ModPath != modpath || vuln.FoundIn != modversion { + continue + } + if len(vuln.Trace) == 0 { + info = append(info, vuln) + } else { + warning = append(warning, vuln) + } + } + sort.Slice(info, func(i, j int) bool { return compareVuln(info[i], info[j]) }) + sort.Slice(warning, func(i, j int) bool { return compareVuln(warning[i], warning[j]) }) + return warning, info +} + +func formatVulnerabilities(affecting, nonaffecting []govulncheck.Vuln, options *source.Options) string { + if len(affecting) == 0 && len(nonaffecting) == 0 { + return "" + } + + // TODO(hyangah): can we use go templates to generate hover messages? + // Then, we can use a different template for markdown case. + useMarkdown := options.PreferredContentFormat == protocol.Markdown + + var b strings.Builder + + if len(affecting) > 0 { + // TODO(hyangah): make the message more eyecatching (icon/codicon/color) + if len(affecting) == 1 { + b.WriteString(fmt.Sprintf("\n**WARNING:** Found %d reachable vulnerability.\n", len(affecting))) + } else { + b.WriteString(fmt.Sprintf("\n**WARNING:** Found %d reachable vulnerabilities.\n", len(affecting))) + } + } + for _, v := range affecting { + fix := "No fix is available." + if v.FixedIn != "" { + fix = "Fixed in " + v.FixedIn + "." + } + + if useMarkdown { + fmt.Fprintf(&b, "- [**%v**](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) + } else { + fmt.Fprintf(&b, " - [%v] %v (%v) %v\n", v.OSV.ID, formatMessage(v), href(v.OSV), fix) + } + } + if len(nonaffecting) > 0 { + fmt.Fprintf(&b, "\n**FYI:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") + } + for _, v := range nonaffecting { + fix := "No fix is available." + if v.FixedIn != "" { + fix = "Fixed in " + v.FixedIn + "." + } + if useMarkdown { + fmt.Fprintf(&b, "- [%v](%v) %v %v\n", v.OSV.ID, href(v.OSV), formatMessage(v), fix) + } else { + fmt.Fprintf(&b, " - [%v] %v %v (%v)\n", v.OSV.ID, formatMessage(v), fix, href(v.OSV)) + } + } + b.WriteString("\n") + return b.String() +} + +func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string { + text = strings.TrimSuffix(text, "\n") + splt := strings.Split(text, "\n") + length := len(splt) + + var b strings.Builder // If the explanation is 2 lines, then it is of the form: // # golang.org/x/text/encoding diff --git a/internal/lsp/mod/mod_test.go b/gopls/internal/lsp/mod/mod_test.go similarity index 86% rename from internal/lsp/mod/mod_test.go rename to gopls/internal/lsp/mod/mod_test.go index b2d257caeeb..767ec44a7d8 100644 --- a/internal/lsp/mod/mod_test.go +++ b/gopls/internal/lsp/mod/mod_test.go @@ -10,10 +10,10 @@ import ( "path/filepath" "testing" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/testenv" ) @@ -26,7 +26,7 @@ func TestModfileRemainsUnchanged(t *testing.T) { testenv.NeedsGo1Point(t, 14) ctx := tests.Context(t) - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) session := cache.NewSession(ctx) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) @@ -46,10 +46,10 @@ func TestModfileRemainsUnchanged(t *testing.T) { t.Fatal(err) } _, _, release, err := session.NewView(ctx, "diagnostics_test", span.URIFromPath(folder), options) - release() if err != nil { t.Fatal(err) } + release() after, err := ioutil.ReadFile(filepath.Join(folder, "go.mod")) if err != nil { t.Fatal(err) diff --git a/internal/lsp/mod/testdata/unchanged/go.mod b/gopls/internal/lsp/mod/testdata/unchanged/go.mod similarity index 100% rename from internal/lsp/mod/testdata/unchanged/go.mod rename to gopls/internal/lsp/mod/testdata/unchanged/go.mod diff --git a/internal/lsp/mod/testdata/unchanged/main.go b/gopls/internal/lsp/mod/testdata/unchanged/main.go similarity index 100% rename from internal/lsp/mod/testdata/unchanged/main.go rename to gopls/internal/lsp/mod/testdata/unchanged/main.go diff --git a/internal/lsp/progress/progress.go b/gopls/internal/lsp/progress/progress.go similarity index 96% rename from internal/lsp/progress/progress.go rename to gopls/internal/lsp/progress/progress.go index d6794cf338b..31a8cb67d1d 100644 --- a/internal/lsp/progress/progress.go +++ b/gopls/internal/lsp/progress/progress.go @@ -13,8 +13,8 @@ import ( "sync" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/xcontext" ) @@ -118,7 +118,7 @@ func (t *Tracker) Start(ctx context.Context, title, message string, token protoc }, }) if err != nil { - event.Error(ctx, "generate progress begin", err) + event.Error(ctx, "progress begin", err) } return wd } @@ -260,8 +260,8 @@ type WorkDoneWriter struct { wd *WorkDone } -func NewWorkDoneWriter(wd *WorkDone) *WorkDoneWriter { - return &WorkDoneWriter{wd: wd} +func NewWorkDoneWriter(ctx context.Context, wd *WorkDone) *WorkDoneWriter { + return &WorkDoneWriter{ctx: ctx, wd: wd} } func (wdw *WorkDoneWriter) Write(p []byte) (n int, err error) { diff --git a/internal/lsp/progress/progress_test.go b/gopls/internal/lsp/progress/progress_test.go similarity index 98% rename from internal/lsp/progress/progress_test.go rename to gopls/internal/lsp/progress/progress_test.go index 6e901d17e97..ef87eba121a 100644 --- a/internal/lsp/progress/progress_test.go +++ b/gopls/internal/lsp/progress/progress_test.go @@ -10,7 +10,7 @@ import ( "sync" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) type fakeClient struct { diff --git a/internal/lsp/protocol/codeactionkind.go b/gopls/internal/lsp/protocol/codeactionkind.go similarity index 100% rename from internal/lsp/protocol/codeactionkind.go rename to gopls/internal/lsp/protocol/codeactionkind.go diff --git a/internal/lsp/protocol/context.go b/gopls/internal/lsp/protocol/context.go similarity index 100% rename from internal/lsp/protocol/context.go rename to gopls/internal/lsp/protocol/context.go diff --git a/internal/lsp/protocol/doc.go b/gopls/internal/lsp/protocol/doc.go similarity index 100% rename from internal/lsp/protocol/doc.go rename to gopls/internal/lsp/protocol/doc.go diff --git a/internal/lsp/protocol/enums.go b/gopls/internal/lsp/protocol/enums.go similarity index 94% rename from internal/lsp/protocol/enums.go rename to gopls/internal/lsp/protocol/enums.go index 434808eeb18..82398e22189 100644 --- a/internal/lsp/protocol/enums.go +++ b/gopls/internal/lsp/protocol/enums.go @@ -10,7 +10,6 @@ import ( var ( namesTextDocumentSyncKind [int(Incremental) + 1]string - namesInitializeError [int(UnknownProtocolVersion) + 1]string namesMessageType [int(Log) + 1]string namesFileChangeType [int(Deleted) + 1]string namesWatchKind [int(WatchDelete) + 1]string @@ -29,8 +28,6 @@ func init() { namesTextDocumentSyncKind[int(Full)] = "Full" namesTextDocumentSyncKind[int(Incremental)] = "Incremental" - namesInitializeError[int(UnknownProtocolVersion)] = "UnknownProtocolVersion" - namesMessageType[int(Error)] = "Error" namesMessageType[int(Warning)] = "Warning" namesMessageType[int(Info)] = "Info" @@ -149,14 +146,6 @@ func ParseTextDocumentSyncKind(s string) TextDocumentSyncKind { return TextDocumentSyncKind(parseEnum(s, namesTextDocumentSyncKind[:])) } -func (e InitializeError) Format(f fmt.State, c rune) { - formatEnum(f, c, int(e), namesInitializeError[:], "InitializeError") -} - -func ParseInitializeError(s string) InitializeError { - return InitializeError(parseEnum(s, namesInitializeError[:])) -} - func (e MessageType) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesMessageType[:], "MessageType") } @@ -173,10 +162,6 @@ func ParseFileChangeType(s string) FileChangeType { return FileChangeType(parseEnum(s, namesFileChangeType[:])) } -func (e WatchKind) Format(f fmt.State, c rune) { - formatEnum(f, c, int(e), namesWatchKind[:], "WatchKind") -} - func ParseWatchKind(s string) WatchKind { return WatchKind(parseEnum(s, namesWatchKind[:])) } diff --git a/gopls/internal/lsp/protocol/generate/compare.go b/gopls/internal/lsp/protocol/generate/compare.go new file mode 100644 index 00000000000..d341307821d --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/compare.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// compare the generated files in two directories diff --git a/gopls/internal/lsp/protocol/generate/data.go b/gopls/internal/lsp/protocol/generate/data.go new file mode 100644 index 00000000000..435f594bcf7 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/data.go @@ -0,0 +1,104 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// various data tables + +// methodNames is a map from the method to the name of the function that handles it +var methodNames = map[string]string{ + "$/cancelRequest": "CancelRequest", + "$/logTrace": "LogTrace", + "$/progress": "Progress", + "$/setTrace": "SetTrace", + "callHierarchy/incomingCalls": "IncomingCalls", + "callHierarchy/outgoingCalls": "OutgoingCalls", + "client/registerCapability": "RegisterCapability", + "client/unregisterCapability": "UnregisterCapability", + "codeAction/resolve": "ResolveCodeAction", + "codeLens/resolve": "ResolveCodeLens", + "completionItem/resolve": "ResolveCompletionItem", + "documentLink/resolve": "ResolveDocumentLink", + "exit": "Exit", + "initialize": "Initialize", + "initialized": "Initialized", + "inlayHint/resolve": "Resolve", + "notebookDocument/didChange": "DidChangeNotebookDocument", + "notebookDocument/didClose": "DidCloseNotebookDocument", + "notebookDocument/didOpen": "DidOpenNotebookDocument", + "notebookDocument/didSave": "DidSaveNotebookDocument", + "shutdown": "Shutdown", + "telemetry/event": "Event", + "textDocument/codeAction": "CodeAction", + "textDocument/codeLens": "CodeLens", + "textDocument/colorPresentation": "ColorPresentation", + "textDocument/completion": "Completion", + "textDocument/declaration": "Declaration", + "textDocument/definition": "Definition", + "textDocument/diagnostic": "Diagnostic", + "textDocument/didChange": "DidChange", + "textDocument/didClose": "DidClose", + "textDocument/didOpen": "DidOpen", + "textDocument/didSave": "DidSave", + "textDocument/documentColor": "DocumentColor", + "textDocument/documentHighlight": "DocumentHighlight", + "textDocument/documentLink": "DocumentLink", + "textDocument/documentSymbol": "DocumentSymbol", + "textDocument/foldingRange": "FoldingRange", + "textDocument/formatting": "Formatting", + "textDocument/hover": "Hover", + "textDocument/implementation": "Implementation", + "textDocument/inlayHint": "InlayHint", + "textDocument/inlineValue": "InlineValue", + "textDocument/linkedEditingRange": "LinkedEditingRange", + "textDocument/moniker": "Moniker", + "textDocument/onTypeFormatting": "OnTypeFormatting", + "textDocument/prepareCallHierarchy": "PrepareCallHierarchy", + "textDocument/prepareRename": "PrepareRename", + "textDocument/prepareTypeHierarchy": "PrepareTypeHierarchy", + "textDocument/publishDiagnostics": "PublishDiagnostics", + "textDocument/rangeFormatting": "RangeFormatting", + "textDocument/references": "References", + "textDocument/rename": "Rename", + "textDocument/selectionRange": "SelectionRange", + "textDocument/semanticTokens/full": "SemanticTokensFull", + "textDocument/semanticTokens/full/delta": "SemanticTokensFullDelta", + "textDocument/semanticTokens/range": "SemanticTokensRange", + "textDocument/signatureHelp": "SignatureHelp", + "textDocument/typeDefinition": "TypeDefinition", + "textDocument/willSave": "WillSave", + "textDocument/willSaveWaitUntil": "WillSaveWaitUntil", + "typeHierarchy/subtypes": "Subtypes", + "typeHierarchy/supertypes": "Supertypes", + "window/logMessage": "LogMessage", + "window/showDocument": "ShowDocument", + "window/showMessage": "ShowMessage", + "window/showMessageRequest": "ShowMessageRequest", + "window/workDoneProgress/cancel": "WorkDoneProgressCancel", + "window/workDoneProgress/create": "WorkDoneProgressCreate", + "workspace/applyEdit": "ApplyEdit", + "workspace/codeLens/refresh": "CodeLensRefresh", + "workspace/configuration": "Configuration", + "workspace/diagnostic": "DiagnosticWorkspace", + "workspace/diagnostic/refresh": "DiagnosticRefresh", + "workspace/didChangeConfiguration": "DidChangeConfiguration", + "workspace/didChangeWatchedFiles": "DidChangeWatchedFiles", + "workspace/didChangeWorkspaceFolders": "DidChangeWorkspaceFolders", + "workspace/didCreateFiles": "DidCreateFiles", + "workspace/didDeleteFiles": "DidDeleteFiles", + "workspace/didRenameFiles": "DidRenameFiles", + "workspace/executeCommand": "ExecuteCommand", + "workspace/inlayHint/refresh": "InlayHintRefresh", + "workspace/inlineValue/refresh": "InlineValueRefresh", + "workspace/semanticTokens/refresh": "SemanticTokensRefresh", + "workspace/symbol": "Symbol", + "workspace/willCreateFiles": "WillCreateFiles", + "workspace/willDeleteFiles": "WillDeleteFiles", + "workspace/willRenameFiles": "WillRenameFiles", + "workspace/workspaceFolders": "WorkspaceFolders", + "workspaceSymbol/resolve": "ResolveWorkspaceSymbol", +} diff --git a/gopls/internal/lsp/protocol/generate/doc.go b/gopls/internal/lsp/protocol/generate/doc.go new file mode 100644 index 00000000000..74685559c8e --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/doc.go @@ -0,0 +1,32 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +/* +GenLSP generates the files tsprotocol.go, tsclient.go, +tsserver.go, tsjson.go that support the language server protocol +for gopls. + +Usage: + + go run . [flags] + +The flags are: + + -d + The directory containing the vscode-languageserver-node repository. + (git clone https://github.com/microsoft/vscode-languageserver-node.git). + If not specified, the default is $HOME/vscode-languageserver-node. + + -o + The directory to write the generated files to. It must exist. + The default is "gen". + + -c + Compare the generated files to the files in the specified directory. + If this flag is not specified, no comparison is done. +*/ +package main diff --git a/gopls/internal/lsp/protocol/generate/generate.go b/gopls/internal/lsp/protocol/generate/generate.go new file mode 100644 index 00000000000..86c332856af --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/generate.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// generate the Go code diff --git a/gopls/internal/lsp/protocol/generate/main.go b/gopls/internal/lsp/protocol/generate/main.go new file mode 100644 index 00000000000..38d25705d32 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/main.go @@ -0,0 +1,93 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "flag" + "fmt" + "log" + "os" +) + +var ( + // git clone https://github.com/microsoft/vscode-languageserver-node.git + repodir = flag.String("d", "", "directory of vscode-languageserver-node") + outputdir = flag.String("o", "gen", "output directory") + cmpolder = flag.String("c", "", "directory of older generated code") +) + +func main() { + log.SetFlags(log.Lshortfile) // log file name and line number, not time + flag.Parse() + + if *repodir == "" { + *repodir = fmt.Sprintf("%s/vscode-languageserver-node", os.Getenv("HOME")) + } + spec := parse(*repodir) + + // index the information in the specification + spec.indexRPCInfo() // messages + spec.indexDefInfo() // named types + +} + +func (s *spec) indexRPCInfo() { + for _, r := range s.model.Requests { + r := r + s.byMethod[r.Method] = &r + } + for _, n := range s.model.Notifications { + n := n + if n.Method == "$/cancelRequest" { + // viewed as too confusing to generate + continue + } + s.byMethod[n.Method] = &n + } +} + +func (sp *spec) indexDefInfo() { + for _, s := range sp.model.Structures { + s := s + sp.byName[s.Name] = &s + } + for _, e := range sp.model.Enumerations { + e := e + sp.byName[e.Name] = &e + } + for _, ta := range sp.model.TypeAliases { + ta := ta + sp.byName[ta.Name] = &ta + } + + // some Structure and TypeAlias names need to be changed for Go + // so byName contains the name used in the .json file, and + // the Name field contains the Go version of the name. + v := sp.model.Structures + for i, s := range v { + switch s.Name { + case "_InitializeParams": // _ is not upper case + v[i].Name = "XInitializeParams" + case "ConfigurationParams": // gopls compatibility + v[i].Name = "ParamConfiguration" + case "InitializeParams": // gopls compatibility + v[i].Name = "ParamInitialize" + case "PreviousResultId": // Go naming convention + v[i].Name = "PreviousResultID" + case "WorkspaceFoldersServerCapabilities": // gopls compatibility + v[i].Name = "WorkspaceFolders5Gn" + } + } + w := sp.model.TypeAliases + for i, t := range w { + switch t.Name { + case "PrepareRenameResult": // gopls compatibility + w[i].Name = "PrepareRename2Gn" + } + } +} diff --git a/gopls/internal/lsp/protocol/generate/main_test.go b/gopls/internal/lsp/protocol/generate/main_test.go new file mode 100644 index 00000000000..d986b59cee9 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/main_test.go @@ -0,0 +1,122 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "testing" +) + +// this is not a test, but an easy way to invoke the debugger +func TestAll(t *testing.T) { + t.Skip("run by hand") + log.SetFlags(log.Lshortfile) + main() +} + +// this is not a test, but an easy way to invoke the debugger +func TestCompare(t *testing.T) { + t.Skip("run by hand") + log.SetFlags(log.Lshortfile) + *cmpolder = "../lsp/gen" // instead use a directory containing the older generated files + main() +} + +// check that the parsed file includes all the information +// from the json file. This test will fail if the spec +// introduces new fields. (one can test this test by +// commenting out some special handling in parse.go.) +func TestParseContents(t *testing.T) { + t.Skip("run by hand") + log.SetFlags(log.Lshortfile) + + // compute our parse of the specification + dir := os.Getenv("HOME") + "/vscode-languageserver-node" + v := parse(dir) + out, err := json.Marshal(v.model) + if err != nil { + t.Fatal(err) + } + var our interface{} + if err := json.Unmarshal(out, &our); err != nil { + t.Fatal(err) + } + + // process the json file + fname := dir + "/protocol/metaModel.json" + buf, err := os.ReadFile(fname) + if err != nil { + t.Fatalf("could not read metaModel.json: %v", err) + } + var raw interface{} + if err := json.Unmarshal(buf, &raw); err != nil { + t.Fatal(err) + } + + // convert to strings showing the fields + them := flatten(raw) + us := flatten(our) + + // everything in them should be in us + lesser := make(sortedMap[bool]) + for _, s := range them { + lesser[s] = true + } + greater := make(sortedMap[bool]) // set of fields we have + for _, s := range us { + greater[s] = true + } + for _, k := range lesser.keys() { // set if fields they have + if !greater[k] { + t.Errorf("missing %s", k) + } + } +} + +// flatten(nil) = "nil" +// flatten(v string) = fmt.Sprintf("%q", v) +// flatten(v float64)= fmt.Sprintf("%g", v) +// flatten(v bool) = fmt.Sprintf("%v", v) +// flatten(v []any) = []string{"[0]"flatten(v[0]), "[1]"flatten(v[1]), ...} +// flatten(v map[string]any) = {"key1": flatten(v["key1"]), "key2": flatten(v["key2"]), ...} +func flatten(x any) []string { + switch v := x.(type) { + case nil: + return []string{"nil"} + case string: + return []string{fmt.Sprintf("%q", v)} + case float64: + return []string{fmt.Sprintf("%g", v)} + case bool: + return []string{fmt.Sprintf("%v", v)} + case []any: + var ans []string + for i, x := range v { + idx := fmt.Sprintf("[%.3d]", i) + for _, s := range flatten(x) { + ans = append(ans, idx+s) + } + } + return ans + case map[string]any: + var ans []string + for k, x := range v { + idx := fmt.Sprintf("%q:", k) + for _, s := range flatten(x) { + ans = append(ans, idx+s) + } + } + return ans + default: + log.Fatalf("unexpected type %T", x) + return nil + } +} diff --git a/gopls/internal/lsp/protocol/generate/naming.go b/gopls/internal/lsp/protocol/generate/naming.go new file mode 100644 index 00000000000..9d9201a49d9 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/naming.go @@ -0,0 +1,64 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// assign names to types. many types come with names, but names +// have to be provided for "or", "and", "tuple", and "literal" types. +// Only one tuple type occurs, so it poses no problem. Otherwise +// the name cannot depend on the ordering of the components, as permuting +// them doesn't change the type. One possibility is to build the name +// of the type out of the names of its components, done in an +// earlier version of this code, but rejected by code reviewers. +// (the name would change if the components changed.) +// An alternate is to use the definition context, which is what is done here +// and works for the existing code. However, it cannot work in general. +// (This easiest case is an "or" type with two "literal" components. +// The components will get the same name, as their definition contexts +// are identical.) spec.byName contains enough information to detect +// such cases. (Note that sometimes giving the same name to different +// types is correct, for instance when they involve stringLiterals.) + +import ( + "strings" +) + +// stacks contain information about the ancestry of a type +// (spaces and initial capital letters are treated specially in stack.name()) +type stack []string + +func (s stack) push(v string) stack { + return append(s, v) +} + +func (s stack) pop() { + s = s[:len(s)-1] +} + +// generate a type name from the stack that contains its ancestry +// +// For instance, ["Result textDocument/implementation"] becomes "_textDocument_implementation" +// which, after being returned, becomes "Or_textDocument_implementation", +// which will become "[]Location" eventually (for gopls compatibility). +func (s stack) name(prefix string) string { + var nm string + var seen int + // use the most recent 2 entries, if there are 2, + // or just the only one. + for i := len(s) - 1; i >= 0 && seen < 2; i-- { + x := s[i] + if x[0] <= 'Z' && x[0] >= 'A' { + // it may contain a message + if idx := strings.Index(x, " "); idx >= 0 { + x = prefix + strings.Replace(x[idx+1:], "/", "_", -1) + } + nm += x + seen++ + } + } + return nm +} diff --git a/gopls/internal/lsp/protocol/generate/output.go b/gopls/internal/lsp/protocol/generate/output.go new file mode 100644 index 00000000000..14a04864e85 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/output.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +// Write the output diff --git a/gopls/internal/lsp/protocol/generate/parse.go b/gopls/internal/lsp/protocol/generate/parse.go new file mode 100644 index 00000000000..9f8067eff4d --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/parse.go @@ -0,0 +1,174 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "time" +) + +// a spec contains the specification of the protocol, and derived information. +type spec struct { + model *Model + + // combined Requests and Notifications, indexed by method (e.g., "textDocument/didOpen") + byMethod sortedMap[Message] + + // Structures, Enumerations, and TypeAliases, indexed by name used in + // the .json specification file + // (Some Structure and Enumeration names need to be changed for Go, + // such as _Initialize) + byName sortedMap[Defined] + + // computed type information + nameToTypes sortedMap[[]*Type] // all the uses of a type name + + // remember which types are in a union type + orTypes sortedMap[sortedMap[bool]] + + // information about the version of vscode-languageclient-node + githash string + modTime time.Time +} + +// parse the specification file and return a spec. +// (TestParseContents checks that the parse gets all the fields of the specification) +func parse(dir string) *spec { + fname := filepath.Join(dir, "protocol", "metaModel.json") + buf, err := os.ReadFile(fname) + if err != nil { + log.Fatalf("could not read metaModel.json: %v", err) + } + // line numbers in the .json file occur as comments in tsprotocol.go + newbuf := addLineNumbers(buf) + var v Model + if err := json.Unmarshal(newbuf, &v); err != nil { + log.Fatalf("could not unmarshal metaModel.json: %v", err) + } + + ans := &spec{ + model: &v, + byMethod: make(sortedMap[Message]), + byName: make(sortedMap[Defined]), + nameToTypes: make(sortedMap[[]*Type]), + orTypes: make(sortedMap[sortedMap[bool]]), + } + ans.githash, ans.modTime = gitInfo(dir) + return ans +} + +// gitInfo returns the git hash and modtime of the repository. +func gitInfo(dir string) (string, time.Time) { + fname := dir + "/.git/HEAD" + buf, err := os.ReadFile(fname) + if err != nil { + log.Fatal(err) + } + buf = bytes.TrimSpace(buf) + var githash string + if len(buf) == 40 { + githash = string(buf[:40]) + } else if bytes.HasPrefix(buf, []byte("ref: ")) { + fname = dir + "/.git/" + string(buf[5:]) + buf, err = os.ReadFile(fname) + if err != nil { + log.Fatal(err) + } + githash = string(buf[:40]) + } else { + log.Fatalf("githash cannot be recovered from %s", fname) + } + loadTime := time.Now() + return githash, loadTime +} + +// addLineNumbers adds a "line" field to each object in the JSON. +func addLineNumbers(buf []byte) []byte { + var ans []byte + // In the specification .json file, the delimiter '{' is + // always followed by a newline. There are other {s embedded in strings. + // json.Token does not return \n, or :, or , so using it would + // require parsing the json to reconstruct the missing information. + for linecnt, i := 1, 0; i < len(buf); i++ { + ans = append(ans, buf[i]) + switch buf[i] { + case '{': + if buf[i+1] == '\n' { + ans = append(ans, fmt.Sprintf(`"line": %d, `, linecnt)...) + // warning: this would fail if the spec file had + // `"value": {\n}`, but it does not, as comma is a separator. + } + case '\n': + linecnt++ + } + } + return ans +} + +// Type.Value has to be treated specially for literals and maps +func (t *Type) UnmarshalJSON(data []byte) error { + // First unmarshal only the unambiguous fields. + var x struct { + Kind string `json:"kind"` + Items []*Type `json:"items"` + Element *Type `json:"element"` + Name string `json:"name"` + Key *Type `json:"key"` + Value any `json:"value"` + Line int `json:"line"` + } + if err := json.Unmarshal(data, &x); err != nil { + return err + } + *t = Type{ + Kind: x.Kind, + Items: x.Items, + Element: x.Element, + Name: x.Name, + Value: x.Value, + Line: x.Line, + } + + // Then unmarshal the 'value' field based on the kind. + // This depends on Unmarshal ignoring fields it doesn't know about. + switch x.Kind { + case "map": + var x struct { + Key *Type `json:"key"` + Value *Type `json:"value"` + } + if err := json.Unmarshal(data, &x); err != nil { + return fmt.Errorf("Type.kind=map: %v", err) + } + t.Key = x.Key + t.Value = x.Value + + case "literal": + var z struct { + Value ParseLiteral `json:"value"` + } + + if err := json.Unmarshal(data, &z); err != nil { + return fmt.Errorf("Type.kind=literal: %v", err) + } + t.Value = z.Value + + case "base", "reference", "array", "and", "or", "tuple", + "stringLiteral": + // nop. never seen integerLiteral or booleanLiteral. + + default: + return fmt.Errorf("cannot decode Type.kind %q: %s", x.Kind, data) + } + return nil +} diff --git a/gopls/internal/lsp/protocol/generate/types.go b/gopls/internal/lsp/protocol/generate/types.go new file mode 100644 index 00000000000..e8abd600815 --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/types.go @@ -0,0 +1,173 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import "sort" + +// Model contains the parsed version of the spec +type Model struct { + Version Metadata `json:"metaData"` + Requests []Request `json:"requests"` + Notifications []Notification `json:"notifications"` + Structures []Structure `json:"structures"` + Enumerations []Enumeration `json:"enumerations"` + TypeAliases []TypeAlias `json:"typeAliases"` + Line int `json:"line"` +} + +// Metadata is information about the version of the spec +type Metadata struct { + Version string `json:"version"` + Line int `json:"line"` +} + +// A Request is the parsed version of an LSP request +type Request struct { + Documentation string `json:"documentation"` + ErrorData *Type `json:"errorData"` + Direction string `json:"messageDirection"` + Method string `json:"method"` + Params *Type `json:"params"` + PartialResult *Type `json:"partialResult"` + Proposed bool `json:"proposed"` + RegistrationMethod string `json:"registrationMethod"` + RegistrationOptions *Type `json:"registrationOptions"` + Result *Type `json:"result"` + Since string `json:"since"` + Line int `json:"line"` +} + +// A Notificatin is the parsed version of an LSP notification +type Notification struct { + Documentation string `json:"documentation"` + Direction string `json:"messageDirection"` + Method string `json:"method"` + Params *Type `json:"params"` + Proposed bool `json:"proposed"` + RegistrationMethod string `json:"registrationMethod"` + RegistrationOptions *Type `json:"registrationOptions"` + Since string `json:"since"` + Line int `json:"line"` +} + +// A Structure is the parsed version of an LSP structure from the spec +type Structure struct { + Documentation string `json:"documentation"` + Extends []*Type `json:"extends"` + Mixins []*Type `json:"mixins"` + Name string `json:"name"` + Properties []NameType `json:"properties"` + Proposed bool `json:"proposed"` + Since string `json:"since"` + Line int `json:"line"` +} + +// An enumeration is the parsed version of an LSP enumeration from the spec +type Enumeration struct { + Documentation string `json:"documentation"` + Name string `json:"name"` + Proposed bool `json:"proposed"` + Since string `json:"since"` + SupportsCustomValues bool `json:"supportsCustomValues"` + Type *Type `json:"type"` + Values []NameValue `json:"values"` + Line int `json:"line"` +} + +// A TypeAlias is the parsed version of an LSP type alias from the spec +type TypeAlias struct { + Documentation string `json:"documentation"` + Name string `json:"name"` + Proposed bool `json:"proposed"` + Since string `json:"since"` + Type *Type `json:"type"` + Line int `json:"line"` +} + +// A NameValue describes an enumeration constant +type NameValue struct { + Documentation string `json:"documentation"` + Name string `json:"name"` + Proposed bool `json:"proposed"` + Since string `json:"since"` + Value any `json:"value"` // number or string + Line int `json:"line"` +} + +// common to Request and Notification +type Message interface { + direction() string +} + +func (r Request) direction() string { + return r.Direction +} + +func (n Notification) direction() string { + return n.Direction +} + +// A Defined is one of Structure, Enumeration, TypeAlias, for type checking +type Defined interface { + tag() +} + +func (s Structure) tag() { +} + +func (e Enumeration) tag() { +} + +func (ta TypeAlias) tag() { +} + +// A Type is the parsed version of an LSP type from the spec, +// or a Type the code constructs +type Type struct { + Kind string `json:"kind"` // -- which kind goes with which field -- + Items []*Type `json:"items"` // "and", "or", "tuple" + Element *Type `json:"element"` // "array" + Name string `json:"name"` // "base", "reference" + Key *Type `json:"key"` // "map" + Value any `json:"value"` // "map", "stringLiteral", "literal" + // used to tie generated code to the specification + Line int `json:"line"` + + name string // these are generated names, like Uint32 + typeName string // these are actual type names, like uint32 +} + +// ParsedLiteral is Type.Value when Type.Kind is "literal" +type ParseLiteral struct { + Properties `json:"properties"` +} + +// A NameType represents the name and type of a structure element +type NameType struct { + Name string `json:"name"` + Type *Type `json:"type"` + Optional bool `json:"optional"` + Documentation string `json:"documentation"` + Since string `json:"since"` + Proposed bool `json:"proposed"` + Line int `json:"line"` +} + +// Properties are the collection of structure elements +type Properties []NameType + +type sortedMap[T any] map[string]T + +func (s sortedMap[T]) keys() []string { + var keys []string + for k := range s { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/gopls/internal/lsp/protocol/generate/utilities.go b/gopls/internal/lsp/protocol/generate/utilities.go new file mode 100644 index 00000000000..b091a0d145f --- /dev/null +++ b/gopls/internal/lsp/protocol/generate/utilities.go @@ -0,0 +1,55 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package main + +import ( + "fmt" + "log" + "runtime" + "strings" + "time" +) + +// goName returns the Go version of a name. +func goName(s string) string { + if s == "" { + return s // doesn't happen + } + s = strings.ToUpper(s[:1]) + s[1:] + if rest := strings.TrimSuffix(s, "Uri"); rest != s { + s = rest + "URI" + } + if rest := strings.TrimSuffix(s, "Id"); rest != s { + s = rest + "ID" + } + return s +} + +// the common header for all generated files +func (s *spec) createHeader() string { + format := `// Copyright 2022 The Go Authors. All rights reserved. + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + + // Code generated for LSP. DO NOT EDIT. + + package protocol + + // Code generated from version %s of protocol/metaModel.json. + // git hash %s (as of %s) + + ` + hdr := fmt.Sprintf(format, s.model.Version.Version, s.githash, s.modTime.Format(time.ANSIC)) + return hdr +} + +// useful in debugging +func here() { + _, f, l, _ := runtime.Caller(1) + log.Printf("here: %s:%d", f, l) +} diff --git a/internal/lsp/protocol/log.go b/gopls/internal/lsp/protocol/log.go similarity index 100% rename from internal/lsp/protocol/log.go rename to gopls/internal/lsp/protocol/log.go diff --git a/internal/lsp/protocol/protocol.go b/gopls/internal/lsp/protocol/protocol.go similarity index 100% rename from internal/lsp/protocol/protocol.go rename to gopls/internal/lsp/protocol/protocol.go diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go new file mode 100644 index 00000000000..2d87c081274 --- /dev/null +++ b/gopls/internal/lsp/protocol/span.go @@ -0,0 +1,318 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// this file contains protocol<->span converters + +// Here's a handy guide for your tour of the location zoo: +// +// Imports: source --> lsppos --> protocol --> span --> token +// +// source.MappedRange = (span.Range, protocol.ColumnMapper) +// +// lsppos.TokenMapper = (token.File, lsppos.Mapper) +// lsppos.Mapper = (line offset table, content) +// +// protocol.ColumnMapper = (URI, token.File, content) +// protocol.Location = (URI, protocol.Range) +// protocol.Range = (start, end Position) +// protocol.Position = (line, char uint32) 0-based UTF-16 +// +// span.Point = (line?, col?, offset?) 1-based UTF-8 +// span.Span = (uri URI, start, end span.Point) +// span.Range = (file token.File, start, end token.Pos) +// +// token.Pos +// token.FileSet +// offset int +// +// TODO(adonovan): simplify this picture. Eliminate the optionality of +// span.{Span,Point}'s position and offset fields: work internally in +// terms of offsets (like span.Range), and require a mapper to convert +// them to protocol (UTF-16) line/col form. Stop honoring //line +// directives. + +package protocol + +import ( + "bytes" + "fmt" + "go/token" + "unicode/utf8" + + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" +) + +// A ColumnMapper maps between UTF-8 oriented positions (e.g. token.Pos, +// span.Span) and the UTF-16 oriented positions used by the LSP. +type ColumnMapper struct { + URI span.URI + TokFile *token.File + Content []byte + + // File content is only really needed for UTF-16 column + // computation, which could be be achieved more compactly. + // For example, one could record only the lines for which + // UTF-16 columns differ from the UTF-8 ones, or only the + // indices of the non-ASCII characters. + // + // TODO(adonovan): consider not retaining the entire file + // content, or at least not exposing the fact that we + // currently retain it. +} + +// NewColumnMapper creates a new column mapper for the given uri and content. +func NewColumnMapper(uri span.URI, content []byte) *ColumnMapper { + tf := span.NewTokenFile(uri.Filename(), content) + return &ColumnMapper{ + URI: uri, + TokFile: tf, + Content: content, + } +} + +func URIFromSpanURI(uri span.URI) DocumentURI { + return DocumentURI(uri) +} + +func URIFromPath(path string) DocumentURI { + return URIFromSpanURI(span.URIFromPath(path)) +} + +func (u DocumentURI) SpanURI() span.URI { + return span.URIFromURI(string(u)) +} + +func (m *ColumnMapper) Location(s span.Span) (Location, error) { + rng, err := m.Range(s) + if err != nil { + return Location{}, err + } + return Location{URI: URIFromSpanURI(s.URI()), Range: rng}, nil +} + +func (m *ColumnMapper) Range(s span.Span) (Range, error) { + if span.CompareURI(m.URI, s.URI()) != 0 { + return Range{}, fmt.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI()) + } + s, err := s.WithOffset(m.TokFile) + if err != nil { + return Range{}, err + } + start, err := m.Position(s.Start()) + if err != nil { + return Range{}, err + } + end, err := m.Position(s.End()) + if err != nil { + return Range{}, err + } + return Range{Start: start, End: end}, nil +} + +// OffsetRange returns a Range for the byte-offset interval Content[start:end], +func (m *ColumnMapper) OffsetRange(start, end int) (Range, error) { + startPosition, err := m.OffsetPosition(start) + if err != nil { + return Range{}, fmt.Errorf("start: %v", err) + } + + endPosition, err := m.OffsetPosition(end) + if err != nil { + return Range{}, fmt.Errorf("end: %v", err) + } + + return Range{Start: startPosition, End: endPosition}, nil +} + +// PosRange returns a protocol Range for the token.Pos interval Content[start:end]. +func (m *ColumnMapper) PosRange(start, end token.Pos) (Range, error) { + startOffset, err := safetoken.Offset(m.TokFile, start) + if err != nil { + return Range{}, fmt.Errorf("start: %v", err) + } + endOffset, err := safetoken.Offset(m.TokFile, end) + if err != nil { + return Range{}, fmt.Errorf("end: %v", err) + } + return m.OffsetRange(startOffset, endOffset) +} + +// Position returns the protocol position for the specified point, +// which must have a byte offset. +func (m *ColumnMapper) Position(p span.Point) (Position, error) { + if !p.HasOffset() { + return Position{}, fmt.Errorf("point is missing offset") + } + return m.OffsetPosition(p.Offset()) +} + +// OffsetPosition returns the protocol position of the specified +// offset within m.Content. +func (m *ColumnMapper) OffsetPosition(offset int) (Position, error) { + // We use span.ToPosition for its "line+1 at EOF" workaround. + // TODO(adonovan): ToPosition honors //line directives. It probably shouldn't. + line, _, err := span.ToPosition(m.TokFile, offset) + if err != nil { + return Position{}, fmt.Errorf("OffsetPosition: %v", err) + } + // If that workaround executed, skip the usual column computation. + char := 0 + if offset != m.TokFile.Size() { + char = m.utf16Column(offset) + } + return Position{ + Line: uint32(line - 1), + Character: uint32(char), + }, nil +} + +// utf16Column returns the zero-based column index of the +// specified file offset, measured in UTF-16 codes. +// Precondition: 0 <= offset <= len(m.Content). +func (m *ColumnMapper) utf16Column(offset int) int { + s := m.Content[:offset] + if i := bytes.LastIndex(s, []byte("\n")); i >= 0 { + s = s[i+1:] + } + // s is the prefix of the line before offset. + return utf16len(s) +} + +// utf16len returns the number of codes in the UTF-16 transcoding of s. +func utf16len(s []byte) int { + var n int + for len(s) > 0 { + n++ + + // Fast path for ASCII. + if s[0] < 0x80 { + s = s[1:] + continue + } + + r, size := utf8.DecodeRune(s) + if r >= 0x10000 { + n++ // surrogate pair + } + s = s[size:] + } + return n +} + +func (m *ColumnMapper) Span(l Location) (span.Span, error) { + return m.RangeSpan(l.Range) +} + +// RangeSpan converts a UTF-16 range to a Span with both the +// position (line/col) and offset fields populated. +func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) { + start, err := m.Point(r.Start) + if err != nil { + return span.Span{}, err + } + end, err := m.Point(r.End) + if err != nil { + return span.Span{}, err + } + return span.New(m.URI, start, end).WithAll(m.TokFile) +} + +func (m *ColumnMapper) RangeToSpanRange(r Range) (span.Range, error) { + spn, err := m.RangeSpan(r) + if err != nil { + return span.Range{}, err + } + return spn.Range(m.TokFile) +} + +// Pos returns the token.Pos of protocol position p within the mapped file. +func (m *ColumnMapper) Pos(p Position) (token.Pos, error) { + start, err := m.Point(p) + if err != nil { + return token.NoPos, err + } + return safetoken.Pos(m.TokFile, start.Offset()) +} + +// Offset returns the utf-8 byte offset of p within the mapped file. +func (m *ColumnMapper) Offset(p Position) (int, error) { + start, err := m.Point(p) + if err != nil { + return 0, err + } + return start.Offset(), nil +} + +// Point returns a span.Point for the protocol position p within the mapped file. +// The resulting point has a valid Position and Offset. +func (m *ColumnMapper) Point(p Position) (span.Point, error) { + line := int(p.Line) + 1 + + // Find byte offset of start of containing line. + offset, err := span.ToOffset(m.TokFile, line, 1) + if err != nil { + return span.Point{}, err + } + lineStart := span.NewPoint(line, 1, offset) + return span.FromUTF16Column(lineStart, int(p.Character)+1, m.Content) +} + +func IsPoint(r Range) bool { + return r.Start.Line == r.End.Line && r.Start.Character == r.End.Character +} + +// CompareRange returns -1 if a is before b, 0 if a == b, and 1 if a is after +// b. +// +// A range a is defined to be 'before' b if a.Start is before b.Start, or +// a.Start == b.Start and a.End is before b.End. +func CompareRange(a, b Range) int { + if r := ComparePosition(a.Start, b.Start); r != 0 { + return r + } + return ComparePosition(a.End, b.End) +} + +// ComparePosition returns -1 if a is before b, 0 if a == b, and 1 if a is +// after b. +func ComparePosition(a, b Position) int { + if a.Line < b.Line { + return -1 + } + if a.Line > b.Line { + return 1 + } + if a.Character < b.Character { + return -1 + } + if a.Character > b.Character { + return 1 + } + return 0 +} + +func Intersect(a, b Range) bool { + if a.Start.Line > b.End.Line || a.End.Line < b.Start.Line { + return false + } + return !((a.Start.Line == b.End.Line) && a.Start.Character > b.End.Character || + (a.End.Line == b.Start.Line) && a.End.Character < b.Start.Character) +} + +// Format implements fmt.Formatter. +// +// Note: Formatter is implemented instead of Stringer (presumably) for +// performance reasons, though it is not clear that it matters in practice. +func (r Range) Format(f fmt.State, _ rune) { + fmt.Fprintf(f, "%v-%v", r.Start, r.End) +} + +// Format implements fmt.Formatter. +// +// See Range.Format for discussion of why the Formatter interface is +// implemented rather than Stringer. +func (p Position) Format(f fmt.State, _ rune) { + fmt.Fprintf(f, "%v:%v", p.Line, p.Character) +} diff --git a/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go similarity index 63% rename from internal/lsp/protocol/tsclient.go rename to gopls/internal/lsp/protocol/tsclient.go index 971a2df72b1..b8f11b50f37 100644 --- a/internal/lsp/protocol/tsclient.go +++ b/gopls/internal/lsp/protocol/tsclient.go @@ -1,220 +1,219 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// Copyright 2019-2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Code generated (see typescript/README.md) DO NOT EDIT. - package protocol -// Package protocol contains data types and code for LSP json rpcs -// generated automatically from vscode-languageserver-node -// commit: 696f9285bf849b73745682fdb1c1feac73eb8772 -// last fetched Fri Apr 01 2022 10:53:41 GMT-0400 (Eastern Daylight Time) +// Code generated from version 3.17.0 of protocol/metaModel.json. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) +// Code generated; DO NOT EDIT. import ( "context" "encoding/json" - "fmt" "golang.org/x/tools/internal/jsonrpc2" ) type Client interface { - ShowMessage(context.Context, *ShowMessageParams) error - LogMessage(context.Context, *LogMessageParams) error - Event(context.Context, *interface{}) error - PublishDiagnostics(context.Context, *PublishDiagnosticsParams) error - Progress(context.Context, *ProgressParams) error - WorkspaceFolders(context.Context) ([]WorkspaceFolder /*WorkspaceFolder[] | null*/, error) - Configuration(context.Context, *ParamConfiguration) ([]LSPAny, error) - WorkDoneProgressCreate(context.Context, *WorkDoneProgressCreateParams) error - ShowDocument(context.Context, *ShowDocumentParams) (*ShowDocumentResult, error) - RegisterCapability(context.Context, *RegistrationParams) error - UnregisterCapability(context.Context, *UnregistrationParams) error - ShowMessageRequest(context.Context, *ShowMessageRequestParams) (*MessageActionItem /*MessageActionItem | null*/, error) - ApplyEdit(context.Context, *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) + LogTrace(context.Context, *LogTraceParams) error // $/logTrace + Progress(context.Context, *ProgressParams) error // $/progress + RegisterCapability(context.Context, *RegistrationParams) error // client/registerCapability + UnregisterCapability(context.Context, *UnregistrationParams) error // client/unregisterCapability + Event(context.Context, *interface{}) error // telemetry/event + PublishDiagnostics(context.Context, *PublishDiagnosticsParams) error // textDocument/publishDiagnostics + LogMessage(context.Context, *LogMessageParams) error // window/logMessage + ShowDocument(context.Context, *ShowDocumentParams) (*ShowDocumentResult, error) // window/showDocument + ShowMessage(context.Context, *ShowMessageParams) error // window/showMessage + ShowMessageRequest(context.Context, *ShowMessageRequestParams) (*MessageActionItem, error) // window/showMessageRequest + WorkDoneProgressCreate(context.Context, *WorkDoneProgressCreateParams) error // window/workDoneProgress/create + ApplyEdit(context.Context, *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) // workspace/applyEdit + CodeLensRefresh(context.Context) error // workspace/codeLens/refresh + Configuration(context.Context, *ParamConfiguration) ([]LSPAny, error) // workspace/configuration + WorkspaceFolders(context.Context) ([]WorkspaceFolder, error) // workspace/workspaceFolders } func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { switch r.Method() { - case "window/showMessage": // notif - var params ShowMessageParams + case "$/logTrace": + var params LogTraceParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.ShowMessage(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "window/logMessage": // notif - var params LogMessageParams + err := client.LogTrace(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "$/progress": + var params ProgressParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.LogMessage(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "telemetry/event": // notif + err := client.Progress(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "client/registerCapability": + var params RegistrationParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + err := client.RegisterCapability(ctx, ¶ms) + return true, reply(ctx, nil, err) // 155 + case "client/unregisterCapability": + var params UnregistrationParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + err := client.UnregisterCapability(ctx, ¶ms) + return true, reply(ctx, nil, err) // 155 + case "telemetry/event": var params interface{} if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.Event(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/publishDiagnostics": // notif + return true, reply(ctx, nil, err) // 231 + case "textDocument/publishDiagnostics": var params PublishDiagnosticsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := client.PublishDiagnostics(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "$/progress": // notif - var params ProgressParams + return true, reply(ctx, nil, err) // 231 + case "window/logMessage": + var params LogMessageParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.Progress(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/workspaceFolders": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - resp, err := client.WorkspaceFolders(ctx) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "workspace/configuration": // req - var params ParamConfiguration + err := client.LogMessage(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "window/showDocument": + var params ShowDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := client.Configuration(ctx, ¶ms) + resp, err := client.ShowDocument(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "window/workDoneProgress/create": // req - var params WorkDoneProgressCreateParams + return true, reply(ctx, resp, nil) // 146 + case "window/showMessage": + var params ShowMessageParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.WorkDoneProgressCreate(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "window/showDocument": // req - var params ShowDocumentParams + err := client.ShowMessage(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "window/showMessageRequest": + var params ShowMessageRequestParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := client.ShowDocument(ctx, ¶ms) + resp, err := client.ShowMessageRequest(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "client/registerCapability": // req - var params RegistrationParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - } - err := client.RegisterCapability(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "client/unregisterCapability": // req - var params UnregistrationParams + return true, reply(ctx, resp, nil) // 146 + case "window/workDoneProgress/create": + var params WorkDoneProgressCreateParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := client.UnregisterCapability(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "window/showMessageRequest": // req - var params ShowMessageRequestParams + err := client.WorkDoneProgressCreate(ctx, ¶ms) + return true, reply(ctx, nil, err) // 155 + case "workspace/applyEdit": + var params ApplyWorkspaceEditParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := client.ShowMessageRequest(ctx, ¶ms) + resp, err := client.ApplyEdit(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/applyEdit": // req - var params ApplyWorkspaceEditParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/codeLens/refresh": + err := client.CodeLensRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/configuration": + var params ParamConfiguration if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := client.ApplyEdit(ctx, ¶ms) + resp, err := client.Configuration(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - + return true, reply(ctx, resp, nil) // 146 + case "workspace/workspaceFolders": + resp, err := client.WorkspaceFolders(ctx) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 165 default: return false, nil } } -func (s *clientDispatcher) ShowMessage(ctx context.Context, params *ShowMessageParams) error { - return s.sender.Notify(ctx, "window/showMessage", params) -} - -func (s *clientDispatcher) LogMessage(ctx context.Context, params *LogMessageParams) error { - return s.sender.Notify(ctx, "window/logMessage", params) -} - +func (s *clientDispatcher) LogTrace(ctx context.Context, params *LogTraceParams) error { + return s.sender.Notify(ctx, "$/logTrace", params) +} // 244 +func (s *clientDispatcher) Progress(ctx context.Context, params *ProgressParams) error { + return s.sender.Notify(ctx, "$/progress", params) +} // 244 +func (s *clientDispatcher) RegisterCapability(ctx context.Context, params *RegistrationParams) error { + return s.sender.Call(ctx, "client/registerCapability", params, nil) +} // 194 +func (s *clientDispatcher) UnregisterCapability(ctx context.Context, params *UnregistrationParams) error { + return s.sender.Call(ctx, "client/unregisterCapability", params, nil) +} // 194 func (s *clientDispatcher) Event(ctx context.Context, params *interface{}) error { return s.sender.Notify(ctx, "telemetry/event", params) -} - +} // 244 func (s *clientDispatcher) PublishDiagnostics(ctx context.Context, params *PublishDiagnosticsParams) error { return s.sender.Notify(ctx, "textDocument/publishDiagnostics", params) -} - -func (s *clientDispatcher) Progress(ctx context.Context, params *ProgressParams) error { - return s.sender.Notify(ctx, "$/progress", params) -} -func (s *clientDispatcher) WorkspaceFolders(ctx context.Context) ([]WorkspaceFolder /*WorkspaceFolder[] | null*/, error) { - var result []WorkspaceFolder /*WorkspaceFolder[] | null*/ - if err := s.sender.Call(ctx, "workspace/workspaceFolders", nil, &result); err != nil { - return nil, err - } - return result, nil -} - -func (s *clientDispatcher) Configuration(ctx context.Context, params *ParamConfiguration) ([]LSPAny, error) { - var result []LSPAny - if err := s.sender.Call(ctx, "workspace/configuration", params, &result); err != nil { - return nil, err - } - return result, nil -} - -func (s *clientDispatcher) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) error { - return s.sender.Call(ctx, "window/workDoneProgress/create", params, nil) // Call, not Notify -} - +} // 244 +func (s *clientDispatcher) LogMessage(ctx context.Context, params *LogMessageParams) error { + return s.sender.Notify(ctx, "window/logMessage", params) +} // 244 func (s *clientDispatcher) ShowDocument(ctx context.Context, params *ShowDocumentParams) (*ShowDocumentResult, error) { var result *ShowDocumentResult if err := s.sender.Call(ctx, "window/showDocument", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *clientDispatcher) RegisterCapability(ctx context.Context, params *RegistrationParams) error { - return s.sender.Call(ctx, "client/registerCapability", params, nil) // Call, not Notify -} - -func (s *clientDispatcher) UnregisterCapability(ctx context.Context, params *UnregistrationParams) error { - return s.sender.Call(ctx, "client/unregisterCapability", params, nil) // Call, not Notify -} - -func (s *clientDispatcher) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (*MessageActionItem /*MessageActionItem | null*/, error) { - var result *MessageActionItem /*MessageActionItem | null*/ +} // 169 +func (s *clientDispatcher) ShowMessage(ctx context.Context, params *ShowMessageParams) error { + return s.sender.Notify(ctx, "window/showMessage", params) +} // 244 +func (s *clientDispatcher) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (*MessageActionItem, error) { + var result *MessageActionItem if err := s.sender.Call(ctx, "window/showMessageRequest", params, &result); err != nil { return nil, err } return result, nil -} - +} // 169 +func (s *clientDispatcher) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) error { + return s.sender.Call(ctx, "window/workDoneProgress/create", params, nil) +} // 194 func (s *clientDispatcher) ApplyEdit(ctx context.Context, params *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) { var result *ApplyWorkspaceEditResult if err := s.sender.Call(ctx, "workspace/applyEdit", params, &result); err != nil { return nil, err } return result, nil -} +} // 169 +func (s *clientDispatcher) CodeLensRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/codeLens/refresh", nil, nil) +} // 209 +func (s *clientDispatcher) Configuration(ctx context.Context, params *ParamConfiguration) ([]LSPAny, error) { + var result []LSPAny + if err := s.sender.Call(ctx, "workspace/configuration", params, &result); err != nil { + return nil, err + } + return result, nil +} // 169 +func (s *clientDispatcher) WorkspaceFolders(ctx context.Context) ([]WorkspaceFolder, error) { + var result []WorkspaceFolder + if err := s.sender.Call(ctx, "workspace/workspaceFolders", nil, &result); err != nil { + return nil, err + } + return result, nil +} // 204 diff --git a/gopls/internal/lsp/protocol/tsdocument_changes.go b/gopls/internal/lsp/protocol/tsdocument_changes.go new file mode 100644 index 00000000000..7296a151ac2 --- /dev/null +++ b/gopls/internal/lsp/protocol/tsdocument_changes.go @@ -0,0 +1,41 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package protocol + +import ( + "encoding/json" + "fmt" +) + +// DocumentChanges is a union of a file edit and directory rename operations +// for package renaming feature. At most one field of this struct is non-nil. +type DocumentChanges struct { + TextDocumentEdit *TextDocumentEdit + RenameFile *RenameFile +} + +func (d *DocumentChanges) UnmarshalJSON(data []byte) error { + var m map[string]interface{} + + if err := json.Unmarshal(data, &m); err != nil { + return err + } + + if _, ok := m["textDocument"]; ok { + d.TextDocumentEdit = new(TextDocumentEdit) + return json.Unmarshal(data, d.TextDocumentEdit) + } + + d.RenameFile = new(RenameFile) + return json.Unmarshal(data, d.RenameFile) +} + +func (d *DocumentChanges) MarshalJSON() ([]byte, error) { + if d.TextDocumentEdit != nil { + return json.Marshal(d.TextDocumentEdit) + } else if d.RenameFile != nil { + return json.Marshal(d.RenameFile) + } + return nil, fmt.Errorf("Empty DocumentChanges union value") +} diff --git a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/lsp/protocol/tsjson.go new file mode 100644 index 00000000000..3c9781ade0b --- /dev/null +++ b/gopls/internal/lsp/protocol/tsjson.go @@ -0,0 +1,440 @@ +// Copyright 2019-2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protocol + +// Code generated from version 3.17.0 of protocol/metaModel.json. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) +// Code generated; DO NOT EDIT. + +import "encoding/json" +import "errors" +import "fmt" + +func (t OrFEditRangePItemDefaults) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case FEditRangePItemDefaults: + return json.Marshal(x) + case Range: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [FEditRangePItemDefaults Range]", t) +} + +func (t *OrFEditRangePItemDefaults) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 FEditRangePItemDefaults + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 Range + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [FEditRangePItemDefaults Range]") +} + +func (t OrFNotebookPNotebookSelector) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case NotebookDocumentFilter: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) +} + +func (t *OrFNotebookPNotebookSelector) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 NotebookDocumentFilter + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [NotebookDocumentFilter string]") +} + +func (t OrPLocation_workspace_symbol) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case Location: + return json.Marshal(x) + case PLocationMsg_workspace_symbol: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [Location PLocationMsg_workspace_symbol]", t) +} + +func (t *OrPLocation_workspace_symbol) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 Location + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 PLocationMsg_workspace_symbol + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [Location PLocationMsg_workspace_symbol]") +} + +func (t OrPSection_workspace_didChangeConfiguration) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case []string: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [[]string string]", t) +} + +func (t *OrPSection_workspace_didChangeConfiguration) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 []string + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [[]string string]") +} + +func (t OrPTooltipPLabel) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case MarkupContent: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +} + +func (t *OrPTooltipPLabel) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 MarkupContent + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [MarkupContent string]") +} + +func (t OrPTooltip_textDocument_inlayHint) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case MarkupContent: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +} + +func (t *OrPTooltip_textDocument_inlayHint) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 MarkupContent + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [MarkupContent string]") +} + +func (t Or_Definition) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case Location: + return json.Marshal(x) + case []Location: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [Location []Location]", t) +} + +func (t *Or_Definition) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 Location + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 []Location + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [Location []Location]") +} + +func (t Or_DocumentDiagnosticReport) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case RelatedFullDocumentDiagnosticReport: + return json.Marshal(x) + case RelatedUnchangedDocumentDiagnosticReport: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]", t) +} + +func (t *Or_DocumentDiagnosticReport) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 RelatedFullDocumentDiagnosticReport + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 RelatedUnchangedDocumentDiagnosticReport + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]") +} + +func (t Or_DocumentFilter) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case NotebookCellTextDocumentFilter: + return json.Marshal(x) + case TextDocumentFilter: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [NotebookCellTextDocumentFilter TextDocumentFilter]", t) +} + +func (t *Or_DocumentFilter) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 NotebookCellTextDocumentFilter + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 TextDocumentFilter + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [NotebookCellTextDocumentFilter TextDocumentFilter]") +} + +func (t Or_InlineValue) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case InlineValueEvaluatableExpression: + return json.Marshal(x) + case InlineValueText: + return json.Marshal(x) + case InlineValueVariableLookup: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]", t) +} + +func (t *Or_InlineValue) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 InlineValueEvaluatableExpression + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 InlineValueText + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + var h2 InlineValueVariableLookup + if err := json.Unmarshal(x, &h2); err == nil { + t.Value = h2 + return nil + } + return errors.New("unmarshal failed to match one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]") +} + +func (t Or_MarkedString) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case Msg_MarkedString: + return json.Marshal(x) + case string: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [Msg_MarkedString string]", t) +} + +func (t *Or_MarkedString) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 Msg_MarkedString + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 string + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [Msg_MarkedString string]") +} + +func (t Or_RelativePattern_baseUri) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case URI: + return json.Marshal(x) + case WorkspaceFolder: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [URI WorkspaceFolder]", t) +} + +func (t *Or_RelativePattern_baseUri) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 URI + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 WorkspaceFolder + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [URI WorkspaceFolder]") +} + +func (t Or_WorkspaceDocumentDiagnosticReport) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case WorkspaceFullDocumentDiagnosticReport: + return json.Marshal(x) + case WorkspaceUnchangedDocumentDiagnosticReport: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]", t) +} + +func (t *Or_WorkspaceDocumentDiagnosticReport) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 WorkspaceFullDocumentDiagnosticReport + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 WorkspaceUnchangedDocumentDiagnosticReport + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]") +} + +func (t Or_textDocument_declaration) MarshalJSON() ([]byte, error) { + switch x := t.Value.(type) { + case Declaration: + return json.Marshal(x) + case []DeclarationLink: + return json.Marshal(x) + case nil: + return []byte("null"), nil + } + return nil, fmt.Errorf("type %T not one of [Declaration []DeclarationLink]", t) +} + +func (t *Or_textDocument_declaration) UnmarshalJSON(x []byte) error { + if string(x) == "null" { + t.Value = nil + return nil + } + var h0 Declaration + if err := json.Unmarshal(x, &h0); err == nil { + t.Value = h0 + return nil + } + var h1 []DeclarationLink + if err := json.Unmarshal(x, &h1); err == nil { + t.Value = h1 + return nil + } + return errors.New("unmarshal failed to match one of [Declaration []DeclarationLink]") +} diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go new file mode 100644 index 00000000000..8272d7e80b3 --- /dev/null +++ b/gopls/internal/lsp/protocol/tsprotocol.go @@ -0,0 +1,6159 @@ +// Copyright 2019-2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package protocol + +// Code generated from version 3.17.0 of protocol/metaModel.json. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) +// Code generated; DO NOT EDIT. + +import "encoding/json" + +/* + * A special text edit with an additional change annotation. + * + * @since 3.16.0. + */ +type AnnotatedTextEdit struct { // line 9392 + // The actual identifier of the change annotation + AnnotationID ChangeAnnotationIdentifier `json:"annotationId"` + TextEdit +} + +// The parameters passed via a apply workspace edit request. +type ApplyWorkspaceEditParams struct { // line 6003 + /* + * An optional label of the workspace edit. This label is + * presented in the user interface for example on an undo + * stack to undo the workspace edit. + */ + Label string `json:"label,omitempty"` + // The edits to apply. + Edit WorkspaceEdit `json:"edit"` +} + +/* + * The result returned from the apply workspace edit request. + * + * @since 3.17 renamed from ApplyWorkspaceEditResponse + */ +type ApplyWorkspaceEditResult struct { // line 6026 + // Indicates whether the edit was applied or not. + Applied bool `json:"applied"` + /* + * An optional textual description for why the edit was not applied. + * This may be used by the server for diagnostic logging or to provide + * a suitable error for a request that triggered the edit. + */ + FailureReason string `json:"failureReason,omitempty"` + /* + * Depending on the client's failure handling strategy `failedChange` might + * contain the index of the change that failed. This property is only available + * if the client signals a `failureHandlingStrategy` in its client capabilities. + */ + FailedChange uint32 `json:"failedChange,omitempty"` +} + +// A base for all symbol information. +type BaseSymbolInformation struct { // line 8986 + // The name of this symbol. + Name string `json:"name"` + // The kind of this symbol. + Kind SymbolKind `json:"kind"` + /* + * Tags for this symbol. + * + * @since 3.16.0 + */ + Tags []SymbolTag `json:"tags,omitempty"` + /* + * The name of the symbol containing this symbol. This information is for + * user interface purposes (e.g. to render a qualifier in the user interface + * if necessary). It can't be used to re-infer a hierarchy for the document + * symbols. + */ + ContainerName string `json:"containerName,omitempty"` +} + +// @since 3.16.0 +type CallHierarchyClientCapabilities struct { // line 12167 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + * return value for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +/* + * Represents an incoming call, e.g. a caller of a method or constructor. + * + * @since 3.16.0 + */ +type CallHierarchyIncomingCall struct { // line 2801 + // The item that makes the call. + From CallHierarchyItem `json:"from"` + /* + * The ranges at which the calls appear. This is relative to the caller + * denoted by [`this.from`](#CallHierarchyIncomingCall.from). + */ + FromRanges []Range `json:"fromRanges"` +} + +/* + * The parameter of a `callHierarchy/incomingCalls` request. + * + * @since 3.16.0 + */ +type CallHierarchyIncomingCallsParams struct { // line 2777 + Item CallHierarchyItem `json:"item"` + WorkDoneProgressParams + PartialResultParams +} + +/* + * Represents programming constructs like functions or constructors in the context + * of call hierarchy. + * + * @since 3.16.0 + */ +type CallHierarchyItem struct { // line 2678 + // The name of this item. + Name string `json:"name"` + // The kind of this item. + Kind SymbolKind `json:"kind"` + // Tags for this item. + Tags []SymbolTag `json:"tags,omitempty"` + // More detail for this item, e.g. the signature of a function. + Detail string `json:"detail,omitempty"` + // The resource identifier of this item. + URI DocumentURI `json:"uri"` + // The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. + Range Range `json:"range"` + /* + * The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. + * Must be contained by the [`range`](#CallHierarchyItem.range). + */ + SelectionRange Range `json:"selectionRange"` + /* + * A data entry field that is preserved between a call hierarchy prepare and + * incoming calls or outgoing calls requests. + */ + Data interface{} `json:"data,omitempty"` +} + +/* + * Call hierarchy options used during static registration. + * + * @since 3.16.0 + */ +type CallHierarchyOptions struct { // line 6539 + WorkDoneProgressOptions +} + +/* + * Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. + * + * @since 3.16.0 + */ +type CallHierarchyOutgoingCall struct { // line 2851 + // The item that is called. + To CallHierarchyItem `json:"to"` + /* + * The range at which this item is called. This is the range relative to the caller, e.g the item + * passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls) + * and not [`this.to`](#CallHierarchyOutgoingCall.to). + */ + FromRanges []Range `json:"fromRanges"` +} + +/* + * The parameter of a `callHierarchy/outgoingCalls` request. + * + * @since 3.16.0 + */ +type CallHierarchyOutgoingCallsParams struct { // line 2827 + Item CallHierarchyItem `json:"item"` + WorkDoneProgressParams + PartialResultParams +} + +/* + * The parameter of a `textDocument/prepareCallHierarchy` request. + * + * @since 3.16.0 + */ +type CallHierarchyPrepareParams struct { // line 2660 + TextDocumentPositionParams + WorkDoneProgressParams +} + +/* + * Call hierarchy options used during static or dynamic registration. + * + * @since 3.16.0 + */ +type CallHierarchyRegistrationOptions struct { // line 2755 + TextDocumentRegistrationOptions + CallHierarchyOptions + StaticRegistrationOptions +} +type CancelParams struct { // line 6198 + // The request id to cancel. + ID interface{} `json:"id"` +} + +/* + * Additional information that describes document changes. + * + * @since 3.16.0 + */ +type ChangeAnnotation struct { // line 6836 + /* + * A human-readable string describing the actual change. The string + * is rendered prominent in the user interface. + */ + Label string `json:"label"` + /* + * A flag which indicates that user confirmation is needed + * before applying the change. + */ + NeedsConfirmation bool `json:"needsConfirmation,omitempty"` + /* + * A human-readable string which is rendered less prominent in + * the user interface. + */ + Description string `json:"description,omitempty"` +} + +// An identifier to refer to a change annotation stored with a workspace edit. +type ChangeAnnotationIdentifier = string // (alias) line 14002 +// Defines the capabilities provided by the client. +type ClientCapabilities struct { // line 9700 + // Workspace specific client capabilities. + Workspace WorkspaceClientCapabilities `json:"workspace,omitempty"` + // Text document specific client capabilities. + TextDocument TextDocumentClientCapabilities `json:"textDocument,omitempty"` + /* + * Capabilities specific to the notebook document support. + * + * @since 3.17.0 + */ + NotebookDocument *NotebookDocumentClientCapabilities `json:"notebookDocument,omitempty"` + // Window specific client capabilities. + Window WindowClientCapabilities `json:"window,omitempty"` + /* + * General client capabilities. + * + * @since 3.16.0 + */ + General *GeneralClientCapabilities `json:"general,omitempty"` + // Experimental client capabilities. + Experimental interface{} `json:"experimental,omitempty"` +} + +/* + * A code action represents a change that can be performed in code, e.g. to fix a problem or + * to refactor code. + * + * A CodeAction must set either `edit` and/or a `command`. If both are supplied, the `edit` is applied first, then the `command` is executed. + */ +type CodeAction struct { // line 5401 + // A short, human-readable, title for this code action. + Title string `json:"title"` + /* + * The kind of the code action. + * + * Used to filter code actions. + */ + Kind CodeActionKind `json:"kind,omitempty"` + // The diagnostics that this code action resolves. + Diagnostics []Diagnostic `json:"diagnostics,omitempty"` + /* + * Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted + * by keybindings. + * + * A quick fix should be marked preferred if it properly addresses the underlying error. + * A refactoring should be marked preferred if it is the most reasonable choice of actions to take. + * + * @since 3.15.0 + */ + IsPreferred bool `json:"isPreferred,omitempty"` + /* + * Marks that the code action cannot currently be applied. + * + * Clients should follow the following guidelines regarding disabled code actions: + * + * - Disabled code actions are not shown in automatic [lightbulbs](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) + * code action menus. + * + * - Disabled actions are shown as faded out in the code action menu when the user requests a more specific type + * of code action, such as refactorings. + * + * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) + * that auto applies a code action and only disabled code actions are returned, the client should show the user an + * error message with `reason` in the editor. + * + * @since 3.16.0 + */ + Disabled *PDisabledMsg_textDocument_codeAction `json:"disabled,omitempty"` + // The workspace edit this code action performs. + Edit WorkspaceEdit `json:"edit,omitempty"` + /* + * A command this code action executes. If a code action + * provides an edit and a command, first the edit is + * executed and then the command. + */ + Command *Command `json:"command,omitempty"` + /* + * A data entry field that is preserved on a code action between + * a `textDocument/codeAction` and a `codeAction/resolve` request. + * + * @since 3.16.0 + */ + Data interface{} `json:"data,omitempty"` +} + +// The Client Capabilities of a [CodeActionRequest](#CodeActionRequest). +type CodeActionClientCapabilities struct { // line 11747 + // Whether code action supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * The client support code action literals of type `CodeAction` as a valid + * response of the `textDocument/codeAction` request. If the property is not + * set the request can only return `Command` literals. + * + * @since 3.8.0 + */ + CodeActionLiteralSupport PCodeActionLiteralSupportPCodeAction `json:"codeActionLiteralSupport,omitempty"` + /* + * Whether code action supports the `isPreferred` property. + * + * @since 3.15.0 + */ + IsPreferredSupport bool `json:"isPreferredSupport,omitempty"` + /* + * Whether code action supports the `disabled` property. + * + * @since 3.16.0 + */ + DisabledSupport bool `json:"disabledSupport,omitempty"` + /* + * Whether code action supports the `data` property which is + * preserved between a `textDocument/codeAction` and a + * `codeAction/resolve` request. + * + * @since 3.16.0 + */ + DataSupport bool `json:"dataSupport,omitempty"` + /* + * Whether the client supports resolving additional code action + * properties via a separate `codeAction/resolve` request. + * + * @since 3.16.0 + */ + ResolveSupport *PResolveSupportPCodeAction `json:"resolveSupport,omitempty"` + /* + * Whether the client honors the change annotations in + * text edits and resource operations returned via the + * `CodeAction#edit` property by for example presenting + * the workspace edit in the user interface and asking + * for confirmation. + * + * @since 3.16.0 + */ + HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` +} + +/* + * Contains additional diagnostic information about the context in which + * a [code action](#CodeActionProvider.provideCodeActions) is run. + */ +type CodeActionContext struct { // line 9052 + /* + * An array of diagnostics known on the client side overlapping the range provided to the + * `textDocument/codeAction` request. They are provided so that the server knows which + * errors are currently presented to the user for the given range. There is no guarantee + * that these accurately reflect the error state of the resource. The primary parameter + * to compute code actions is the provided range. + */ + Diagnostics []Diagnostic `json:"diagnostics"` + /* + * Requested kind of actions to return. + * + * Actions not of this kind are filtered out by the client before being shown. So servers + * can omit computing them. + */ + Only []CodeActionKind `json:"only,omitempty"` + /* + * The reason why code actions were requested. + * + * @since 3.17.0 + */ + TriggerKind CodeActionTriggerKind `json:"triggerKind,omitempty"` +} +type CodeActionKind string // line 13352 +// Provider options for a [CodeActionRequest](#CodeActionRequest). +type CodeActionOptions struct { // line 9091 + /* + * CodeActionKinds that this server may return. + * + * The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server + * may list out every specific kind they provide. + */ + CodeActionKinds []CodeActionKind `json:"codeActionKinds,omitempty"` + /* + * The server provides support to resolve additional + * information for a code action. + * + * @since 3.16.0 + */ + ResolveProvider bool `json:"resolveProvider,omitempty"` + WorkDoneProgressOptions +} + +// The parameters of a [CodeActionRequest](#CodeActionRequest). +type CodeActionParams struct { // line 5327 + // The document in which the command was invoked. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The range for which the command was invoked. + Range Range `json:"range"` + // Context carrying additional information. + Context CodeActionContext `json:"context"` + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [CodeActionRequest](#CodeActionRequest). +type CodeActionRegistrationOptions struct { // line 5495 + TextDocumentRegistrationOptions + CodeActionOptions +} +type CodeActionTriggerKind uint32 // line 13632 +/* + * Structure to capture a description for an error code. + * + * @since 3.16.0 + */ +type CodeDescription struct { // line 10052 + // An URI to open with more information about the diagnostic error. + Href URI `json:"href"` +} + +/* + * A code lens represents a [command](#Command) that should be shown along with + * source text, like the number of references, a way to run tests, etc. + * + * A code lens is _unresolved_ when no command is associated to it. For performance + * reasons the creation of a code lens and resolving should be done in two stages. + */ +type CodeLens struct { // line 5618 + // The range in which this code lens is valid. Should only span a single line. + Range Range `json:"range"` + // The command this code lens represents. + Command Command `json:"command,omitempty"` + /* + * A data entry field that is preserved on a code lens item between + * a [CodeLensRequest](#CodeLensRequest) and a [CodeLensResolveRequest] + * (#CodeLensResolveRequest) + */ + Data interface{} `json:"data,omitempty"` +} + +// The client capabilities of a [CodeLensRequest](#CodeLensRequest). +type CodeLensClientCapabilities struct { // line 11861 + // Whether code lens supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +// Code Lens provider options of a [CodeLensRequest](#CodeLensRequest). +type CodeLensOptions struct { // line 9147 + // Code lens has a resolve provider as well. + ResolveProvider bool `json:"resolveProvider,omitempty"` + WorkDoneProgressOptions +} + +// The parameters of a [CodeLensRequest](#CodeLensRequest). +type CodeLensParams struct { // line 5594 + // The document to request code lens for. + TextDocument TextDocumentIdentifier `json:"textDocument"` + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [CodeLensRequest](#CodeLensRequest). +type CodeLensRegistrationOptions struct { // line 5650 + TextDocumentRegistrationOptions + CodeLensOptions +} + +// @since 3.16.0 +type CodeLensWorkspaceClientCapabilities struct { // line 11019 + /* + * Whether the client implementation supports a refresh request sent from the + * server to the client. + * + * Note that this event is global and will force the client to refresh all + * code lenses currently shown. It should be used with absolute care and is + * useful for situation where a server for example detect a project wide + * change that requires such a calculation. + */ + RefreshSupport bool `json:"refreshSupport,omitempty"` +} + +// Represents a color in RGBA space. +type Color struct { // line 6438 + // The red component of this color in the range [0-1]. + Red float64 `json:"red"` + // The green component of this color in the range [0-1]. + Green float64 `json:"green"` + // The blue component of this color in the range [0-1]. + Blue float64 `json:"blue"` + // The alpha component of this color in the range [0-1]. + Alpha float64 `json:"alpha"` +} + +// Represents a color range from a document. +type ColorInformation struct { // line 2261 + // The range in the document where this color appears. + Range Range `json:"range"` + // The actual color value for this color range. + Color Color `json:"color"` +} +type ColorPresentation struct { // line 2343 + /* + * The label of this color presentation. It will be shown on the color + * picker header. By default this is also the text that is inserted when selecting + * this color presentation. + */ + Label string `json:"label"` + /* + * An [edit](#TextEdit) which is applied to a document when selecting + * this presentation for the color. When `falsy` the [label](#ColorPresentation.label) + * is used. + */ + TextEdit *TextEdit `json:"textEdit,omitempty"` + /* + * An optional array of additional [text edits](#TextEdit) that are applied when + * selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. + */ + AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` +} + +// Parameters for a [ColorPresentationRequest](#ColorPresentationRequest). +type ColorPresentationParams struct { // line 2303 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The color to request presentations for. + Color Color `json:"color"` + // The range where the color would be inserted. Serves as a context. + Range Range `json:"range"` + WorkDoneProgressParams + PartialResultParams +} + +/* + * Represents a reference to a command. Provides a title which + * will be used to represent a command in the UI and, optionally, + * an array of arguments which will be passed to the command handler + * function when invoked. + */ +type Command struct { // line 5367 + // Title of the command, like `save`. + Title string `json:"title"` + // The identifier of the actual command handler. + Command string `json:"command"` + /* + * Arguments that the command handler should be + * invoked with. + */ + Arguments []json.RawMessage `json:"arguments,omitempty"` +} + +// Completion client capabilities +type CompletionClientCapabilities struct { // line 11194 + // Whether completion supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * The client supports the following `CompletionItem` specific + * capabilities. + */ + CompletionItem PCompletionItemPCompletion `json:"completionItem,omitempty"` + CompletionItemKind *PCompletionItemKindPCompletion `json:"completionItemKind,omitempty"` + /* + * Defines how the client handles whitespace and indentation + * when accepting a completion item that uses multi line + * text in either `insertText` or `textEdit`. + * + * @since 3.17.0 + */ + InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` + /* + * The client supports to send additional context information for a + * `textDocument/completion` request. + */ + ContextSupport bool `json:"contextSupport,omitempty"` + /* + * The client supports the following `CompletionList` specific + * capabilities. + * + * @since 3.17.0 + */ + CompletionList *PCompletionListPCompletion `json:"completionList,omitempty"` +} + +// Contains additional information about the context in which a completion request is triggered. +type CompletionContext struct { // line 8648 + // How the completion was triggered. + TriggerKind CompletionTriggerKind `json:"triggerKind"` + /* + * The trigger character (a single character) that has trigger code complete. + * Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` + */ + TriggerCharacter string `json:"triggerCharacter,omitempty"` +} + +/* + * A completion item represents a text snippet that is + * proposed to complete text that is being typed. + */ +type CompletionItem struct { // line 4550 + /* + * The label of this completion item. + * + * The label property is also by default the text that + * is inserted when selecting this completion. + * + * If label details are provided the label itself should + * be an unqualified name of the completion item. + */ + Label string `json:"label"` + /* + * Additional details for the label + * + * @since 3.17.0 + */ + LabelDetails *CompletionItemLabelDetails `json:"labelDetails,omitempty"` + /* + * The kind of this completion item. Based of the kind + * an icon is chosen by the editor. + */ + Kind CompletionItemKind `json:"kind,omitempty"` + /* + * Tags for this completion item. + * + * @since 3.15.0 + */ + Tags []CompletionItemTag `json:"tags,omitempty"` + /* + * A human-readable string with additional information + * about this item, like type or symbol information. + */ + Detail string `json:"detail,omitempty"` + // A human-readable string that represents a doc-comment. + Documentation string `json:"documentation,omitempty"` + /* + * Indicates if this item is deprecated. + * @deprecated Use `tags` instead. + */ + Deprecated bool `json:"deprecated,omitempty"` + /* + * Select this item when showing. + * + * *Note* that only one completion item can be selected and that the + * tool / client decides which item that is. The rule is that the *first* + * item of those that match best is selected. + */ + Preselect bool `json:"preselect,omitempty"` + /* + * A string that should be used when comparing this item + * with other items. When `falsy` the [label](#CompletionItem.label) + * is used. + */ + SortText string `json:"sortText,omitempty"` + /* + * A string that should be used when filtering a set of + * completion items. When `falsy` the [label](#CompletionItem.label) + * is used. + */ + FilterText string `json:"filterText,omitempty"` + /* + * A string that should be inserted into a document when selecting + * this completion. When `falsy` the [label](#CompletionItem.label) + * is used. + * + * The `insertText` is subject to interpretation by the client side. + * Some tools might not take the string literally. For example + * VS Code when code complete is requested in this example + * `con` and a completion item with an `insertText` of + * `console` is provided it will only insert `sole`. Therefore it is + * recommended to use `textEdit` instead since it avoids additional client + * side interpretation. + */ + InsertText string `json:"insertText,omitempty"` + /* + * The format of the insert text. The format applies to both the + * `insertText` property and the `newText` property of a provided + * `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`. + * + * Please note that the insertTextFormat doesn't apply to + * `additionalTextEdits`. + */ + InsertTextFormat InsertTextFormat `json:"insertTextFormat,omitempty"` + /* + * How whitespace and indentation is handled during completion + * item insertion. If not provided the clients default value depends on + * the `textDocument.completion.insertTextMode` client capability. + * + * @since 3.16.0 + */ + InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` + /* + * An [edit](#TextEdit) which is applied to a document when selecting + * this completion. When an edit is provided the value of + * [insertText](#CompletionItem.insertText) is ignored. + * + * Most editors support two different operations when accepting a completion + * item. One is to insert a completion text and the other is to replace an + * existing text with a completion text. Since this can usually not be + * predetermined by a server it can report both ranges. Clients need to + * signal support for `InsertReplaceEdits` via the + * `textDocument.completion.insertReplaceSupport` client capability + * property. + * + * *Note 1:* The text edit's range as well as both ranges from an insert + * replace edit must be a [single line] and they must contain the position + * at which completion has been requested. + * *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range + * must be a prefix of the edit's replace range, that means it must be + * contained and starting at the same position. + * + * @since 3.16.0 additional type `InsertReplaceEdit` + */ + TextEdit *TextEdit `json:"textEdit,omitempty"` + /* + * The edit text used if the completion item is part of a CompletionList and + * CompletionList defines an item default for the text edit range. + * + * Clients will only honor this property if they opt into completion list + * item defaults using the capability `completionList.itemDefaults`. + * + * If not provided and a list's default range is provided the label + * property is used as a text. + * + * @since 3.17.0 + */ + TextEditText string `json:"textEditText,omitempty"` + /* + * An optional array of additional [text edits](#TextEdit) that are applied when + * selecting this completion. Edits must not overlap (including the same insert position) + * with the main [edit](#CompletionItem.textEdit) nor with themselves. + * + * Additional text edits should be used to change text unrelated to the current cursor position + * (for example adding an import statement at the top of the file if the completion item will + * insert an unqualified type). + */ + AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` + /* + * An optional set of characters that when pressed while this completion is active will accept it first and + * then type that character. *Note* that all commit characters should have `length=1` and that superfluous + * characters will be ignored. + */ + CommitCharacters []string `json:"commitCharacters,omitempty"` + /* + * An optional [command](#Command) that is executed *after* inserting this completion. *Note* that + * additional modifications to the current document should be described with the + * [additionalTextEdits](#CompletionItem.additionalTextEdits)-property. + */ + Command *Command `json:"command,omitempty"` + /* + * A data entry field that is preserved on a completion item between a + * [CompletionRequest](#CompletionRequest) and a [CompletionResolveRequest](#CompletionResolveRequest). + */ + Data interface{} `json:"data,omitempty"` +} +type CompletionItemKind uint32 // line 13160 +/* + * Additional details for a completion item label. + * + * @since 3.17.0 + */ +type CompletionItemLabelDetails struct { // line 8671 + /* + * An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, + * without any spacing. Should be used for function signatures and type annotations. + */ + Detail string `json:"detail,omitempty"` + /* + * An optional string which is rendered less prominently after {@link CompletionItem.detail}. Should be used + * for fully qualified names and file paths. + */ + Description string `json:"description,omitempty"` +} +type CompletionItemTag uint32 // line 13270 +/* + * Represents a collection of [completion items](#CompletionItem) to be presented + * in the editor. + */ +type CompletionList struct { // line 4758 + /* + * This list it not complete. Further typing results in recomputing this list. + * + * Recomputed lists have all their items replaced (not appended) in the + * incomplete completion sessions. + */ + IsIncomplete bool `json:"isIncomplete"` + /* + * In many cases the items of an actual completion result share the same + * value for properties like `commitCharacters` or the range of a text + * edit. A completion list can therefore define item defaults which will + * be used if a completion item itself doesn't specify the value. + * + * If a completion list specifies a default value and a completion item + * also specifies a corresponding value the one from the item is used. + * + * Servers are only allowed to return default values if the client + * signals support for this via the `completionList.itemDefaults` + * capability. + * + * @since 3.17.0 + */ + ItemDefaults *PItemDefaultsMsg_textDocument_completion `json:"itemDefaults,omitempty"` + // The completion items. + Items []CompletionItem `json:"items"` +} + +// Completion options. +type CompletionOptions struct { // line 8727 + /* + * Most tools trigger completion request automatically without explicitly requesting + * it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user + * starts to type an identifier. For example if the user types `c` in a JavaScript file + * code complete will automatically pop up present `console` besides others as a + * completion item. Characters that make up identifiers don't need to be listed here. + * + * If code complete should automatically be trigger on characters not being valid inside + * an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. + */ + TriggerCharacters []string `json:"triggerCharacters,omitempty"` + /* + * The list of all possible characters that commit a completion. This field can be used + * if clients don't support individual commit characters per completion item. See + * `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` + * + * If a server provides both `allCommitCharacters` and commit characters on an individual + * completion item the ones on the completion item win. + * + * @since 3.2.0 + */ + AllCommitCharacters []string `json:"allCommitCharacters,omitempty"` + /* + * The server provides support to resolve additional + * information for a completion item. + */ + ResolveProvider bool `json:"resolveProvider,omitempty"` + /* + * The server supports the following `CompletionItem` specific + * capabilities. + * + * @since 3.17.0 + */ + CompletionItem *PCompletionItemPCompletionProvider `json:"completionItem,omitempty"` + WorkDoneProgressOptions +} + +// Completion parameters +type CompletionParams struct { // line 4519 + /* + * The completion context. This is only available it the client specifies + * to send this using the client capability `textDocument.completion.contextSupport === true` + */ + Context CompletionContext `json:"context,omitempty"` + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [CompletionRequest](#CompletionRequest). +type CompletionRegistrationOptions struct { // line 4875 + TextDocumentRegistrationOptions + CompletionOptions +} +type CompletionTriggerKind uint32 // line 13581 +type ConfigurationItem struct { // line 6401 + // The scope to get the configuration section for. + ScopeURI string `json:"scopeUri,omitempty"` + // The configuration section asked for. + Section string `json:"section,omitempty"` +} + +// The parameters of a configuration request. +type ConfigurationParams struct { // line 2207 + Items []ConfigurationItem `json:"items"` +} + +// Create file operation. +type CreateFile struct { // line 6717 + // A create + Kind string `json:"kind"` + // The resource to create. + URI DocumentURI `json:"uri"` + // Additional options + Options *CreateFileOptions `json:"options,omitempty"` + ResourceOperation +} + +// Options to create a file. +type CreateFileOptions struct { // line 9437 + // Overwrite existing file. Overwrite wins over `ignoreIfExists` + Overwrite bool `json:"overwrite,omitempty"` + // Ignore if exists. + IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` +} + +/* + * The parameters sent in notifications/requests for user-initiated creation of + * files. + * + * @since 3.16.0 + */ +type CreateFilesParams struct { // line 3197 + // An array of all files/folders created in this operation. + Files []FileCreate `json:"files"` +} + +// The declaration of a symbol representation as one or many [locations](#Location). +type Declaration = []Location // (alias) line 13859 +// @since 3.14.0 +type DeclarationClientCapabilities struct { // line 11535 + /* + * Whether declaration supports dynamic registration. If this is set to `true` + * the client supports the new `DeclarationRegistrationOptions` return value + * for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + // The client supports additional metadata in the form of declaration links. + LinkSupport bool `json:"linkSupport,omitempty"` +} + +/* + * Information about where a symbol is declared. + * + * Provides additional metadata over normal [location](#Location) declarations, including the range of + * the declaring symbol. + * + * Servers should prefer returning `DeclarationLink` over `Declaration` if supported + * by the client. + */ +type DeclarationLink = LocationLink // (alias) line 13879 +type DeclarationOptions struct { // line 6496 + WorkDoneProgressOptions +} +type DeclarationParams struct { // line 2516 + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} +type DeclarationRegistrationOptions struct { // line 2536 + DeclarationOptions + TextDocumentRegistrationOptions + StaticRegistrationOptions +} + +/* + * The definition of a symbol represented as one or many [locations](#Location). + * For most programming languages there is only one location at which a symbol is + * defined. + * + * Servers should prefer returning `DefinitionLink` over `Definition` if supported + * by the client. + */ +type Definition = Or_Definition // (alias) line 13777 +// Client Capabilities for a [DefinitionRequest](#DefinitionRequest). +type DefinitionClientCapabilities struct { // line 11560 + // Whether definition supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * The client supports additional metadata in the form of definition links. + * + * @since 3.14.0 + */ + LinkSupport bool `json:"linkSupport,omitempty"` +} + +/* + * Information about where a symbol is defined. + * + * Provides additional metadata over normal [location](#Location) definitions, including the range of + * the defining symbol + */ +type DefinitionLink = LocationLink // (alias) line 13797 +// Server Capabilities for a [DefinitionRequest](#DefinitionRequest). +type DefinitionOptions struct { // line 8939 + WorkDoneProgressOptions +} + +// Parameters for a [DefinitionRequest](#DefinitionRequest). +type DefinitionParams struct { // line 5039 + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [DefinitionRequest](#DefinitionRequest). +type DefinitionRegistrationOptions struct { // line 5060 + TextDocumentRegistrationOptions + DefinitionOptions +} + +// Delete file operation +type DeleteFile struct { // line 6799 + // A delete + Kind string `json:"kind"` + // The file to delete. + URI DocumentURI `json:"uri"` + // Delete options. + Options *DeleteFileOptions `json:"options,omitempty"` + ResourceOperation +} + +// Delete file options +type DeleteFileOptions struct { // line 9485 + // Delete the content recursively if a folder is denoted. + Recursive bool `json:"recursive,omitempty"` + // Ignore the operation if the file doesn't exist. + IgnoreIfNotExists bool `json:"ignoreIfNotExists,omitempty"` +} + +/* + * The parameters sent in notifications/requests for user-initiated deletes of + * files. + * + * @since 3.16.0 + */ +type DeleteFilesParams struct { // line 3322 + // An array of all files/folders deleted in this operation. + Files []FileDelete `json:"files"` +} + +/* + * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects + * are only valid in the scope of a resource. + */ +type Diagnostic struct { // line 8545 + // The range at which the message applies + Range Range `json:"range"` + /* + * The diagnostic's severity. Can be omitted. If omitted it is up to the + * client to interpret diagnostics as error, warning, info or hint. + */ + Severity DiagnosticSeverity `json:"severity,omitempty"` + // The diagnostic's code, which usually appear in the user interface. + Code interface{} `json:"code,omitempty"` + /* + * An optional property to describe the error code. + * Requires the code field (above) to be present/not null. + * + * @since 3.16.0 + */ + CodeDescription *CodeDescription `json:"codeDescription,omitempty"` + /* + * A human-readable string describing the source of this + * diagnostic, e.g. 'typescript' or 'super lint'. It usually + * appears in the user interface. + */ + Source string `json:"source,omitempty"` + // The diagnostic's message. It usually appears in the user interface + Message string `json:"message"` + /* + * Additional metadata about the diagnostic. + * + * @since 3.15.0 + */ + Tags []DiagnosticTag `json:"tags,omitempty"` + /* + * An array of related diagnostic information, e.g. when symbol-names within + * a scope collide all definitions can be marked via this property. + */ + RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"` + /* + * A data entry field that is preserved between a `textDocument/publishDiagnostics` + * notification and `textDocument/codeAction` request. + * + * @since 3.16.0 + */ + Data interface{} `json:"data,omitempty"` +} + +/* + * Client capabilities specific to diagnostic pull requests. + * + * @since 3.17.0 + */ +type DiagnosticClientCapabilities struct { // line 12434 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + * return value for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + // Whether the clients supports related documents for document diagnostic pulls. + RelatedDocumentSupport bool `json:"relatedDocumentSupport,omitempty"` +} + +/* + * Diagnostic options. + * + * @since 3.17.0 + */ +type DiagnosticOptions struct { // line 7298 + /* + * An optional identifier under which the diagnostics are + * managed by the client. + */ + Identifier string `json:"identifier,omitempty"` + /* + * Whether the language has inter file dependencies meaning that + * editing code in one file can result in a different diagnostic + * set in another file. Inter file dependencies are common for + * most programming languages and typically uncommon for linters. + */ + InterFileDependencies bool `json:"interFileDependencies"` + // The server provides support for workspace diagnostics as well. + WorkspaceDiagnostics bool `json:"workspaceDiagnostics"` + WorkDoneProgressOptions +} + +/* + * Diagnostic registration options. + * + * @since 3.17.0 + */ +type DiagnosticRegistrationOptions struct { // line 3877 + TextDocumentRegistrationOptions + DiagnosticOptions + StaticRegistrationOptions +} + +/* + * Represents a related message and source code location for a diagnostic. This should be + * used to point to code locations that cause or related to a diagnostics, e.g when duplicating + * a symbol in a scope. + */ +type DiagnosticRelatedInformation struct { // line 10067 + // The location of this related diagnostic information. + Location Location `json:"location"` + // The message of this related diagnostic information. + Message string `json:"message"` +} + +/* + * Cancellation data returned from a diagnostic request. + * + * @since 3.17.0 + */ +type DiagnosticServerCancellationData struct { // line 3863 + RetriggerRequest bool `json:"retriggerRequest"` +} +type DiagnosticSeverity uint32 // line 13530 +type DiagnosticTag uint32 // line 13560 +/* + * Workspace client capabilities specific to diagnostic pull requests. + * + * @since 3.17.0 + */ +type DiagnosticWorkspaceClientCapabilities struct { // line 11137 + /* + * Whether the client implementation supports a refresh request sent from + * the server to the client. + * + * Note that this event is global and will force the client to refresh all + * pulled diagnostics currently shown. It should be used with absolute care and + * is useful for situation where a server for example detects a project wide + * change that requires such a calculation. + */ + RefreshSupport bool `json:"refreshSupport,omitempty"` +} +type DidChangeConfigurationClientCapabilities struct { // line 10863 + // Did change configuration notification supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +// The parameters of a change configuration notification. +type DidChangeConfigurationParams struct { // line 4166 + // The actual changed settings + Settings interface{} `json:"settings"` +} +type DidChangeConfigurationRegistrationOptions struct { // line 4180 + Section *OrPSection_workspace_didChangeConfiguration `json:"section,omitempty"` +} + +/* + * The params sent in a change notebook document notification. + * + * @since 3.17.0 + */ +type DidChangeNotebookDocumentParams struct { // line 3996 + /* + * The notebook document that did change. The version number points + * to the version after all provided changes have been applied. If + * only the text document content of a cell changes the notebook version + * doesn't necessarily have to change. + */ + NotebookDocument VersionedNotebookDocumentIdentifier `json:"notebookDocument"` + /* + * The actual changes to the notebook document. + * + * The changes describe single state changes to the notebook document. + * So if there are two changes c1 (at array index 0) and c2 (at array + * index 1) for a notebook in state S then c1 moves the notebook from + * S to S' and c2 from S' to S''. So c1 is computed on the state S and + * c2 is computed on the state S'. + * + * To mirror the content of a notebook using change events use the following approach: + * - start with the same initial content + * - apply the 'notebookDocument/didChange' notifications in the order you receive them. + * - apply the `NotebookChangeEvent`s in a single notification in the order + * you receive them. + */ + Change NotebookDocumentChangeEvent `json:"change"` +} + +// The change text document notification's parameters. +type DidChangeTextDocumentParams struct { // line 4309 + /* + * The document that did change. The version number points + * to the version after all provided content changes have + * been applied. + */ + TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` + /* + * The actual content changes. The content changes describe single state changes + * to the document. So if there are two content changes c1 (at array index 0) and + * c2 (at array index 1) for a document in state S then c1 moves the document from + * S to S' and c2 from S' to S''. So c1 is computed on the state S and c2 is computed + * on the state S'. + * + * To mirror the content of a document using change events use the following approach: + * - start with the same initial content + * - apply the 'textDocument/didChange' notifications in the order you receive them. + * - apply the `TextDocumentContentChangeEvent`s in a single notification in the order + * you receive them. + */ + ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` +} +type DidChangeWatchedFilesClientCapabilities struct { // line 10877 + /* + * Did change watched files notification supports dynamic registration. Please note + * that the current protocol doesn't support static configuration for file changes + * from the server side. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * Whether the client has support for {@link RelativePattern relative pattern} + * or not. + * + * @since 3.17.0 + */ + RelativePatternSupport bool `json:"relativePatternSupport,omitempty"` +} + +// The watched files change notification's parameters. +type DidChangeWatchedFilesParams struct { // line 4450 + // The actual file events. + Changes []FileEvent `json:"changes"` +} + +// Describe options to be used when registered for text document change events. +type DidChangeWatchedFilesRegistrationOptions struct { // line 4467 + // The watchers to register. + Watchers []FileSystemWatcher `json:"watchers"` +} + +// The parameters of a `workspace/didChangeWorkspaceFolders` notification. +type DidChangeWorkspaceFoldersParams struct { // line 2193 + // The actual workspace folder change event. + Event WorkspaceFoldersChangeEvent `json:"event"` +} + +/* + * The params sent in a close notebook document notification. + * + * @since 3.17.0 + */ +type DidCloseNotebookDocumentParams struct { // line 4034 + // The notebook document that got closed. + NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` + /* + * The text documents that represent the content + * of a notebook cell that got closed. + */ + CellTextDocuments []TextDocumentIdentifier `json:"cellTextDocuments"` +} + +// The parameters sent in a close text document notification +type DidCloseTextDocumentParams struct { // line 4354 + // The document that was closed. + TextDocument TextDocumentIdentifier `json:"textDocument"` +} + +/* + * The params sent in an open notebook document notification. + * + * @since 3.17.0 + */ +type DidOpenNotebookDocumentParams struct { // line 3970 + // The notebook document that got opened. + NotebookDocument NotebookDocument `json:"notebookDocument"` + /* + * The text documents that represent the content + * of a notebook cell. + */ + CellTextDocuments []TextDocumentItem `json:"cellTextDocuments"` +} + +// The parameters sent in an open text document notification +type DidOpenTextDocumentParams struct { // line 4295 + // The document that was opened. + TextDocument TextDocumentItem `json:"textDocument"` +} + +/* + * The params sent in a save notebook document notification. + * + * @since 3.17.0 + */ +type DidSaveNotebookDocumentParams struct { // line 4019 + // The notebook document that got saved. + NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` +} + +// The parameters sent in a save text document notification +type DidSaveTextDocumentParams struct { // line 4368 + // The document that was saved. + TextDocument TextDocumentIdentifier `json:"textDocument"` + /* + * Optional the content when saved. Depends on the includeText value + * when the save notification was requested. + */ + Text *string `json:"text,omitempty"` +} +type DocumentColorClientCapabilities struct { // line 11901 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `DocumentColorRegistrationOptions` return value + * for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} +type DocumentColorOptions struct { // line 6476 + WorkDoneProgressOptions +} + +// Parameters for a [DocumentColorRequest](#DocumentColorRequest). +type DocumentColorParams struct { // line 2237 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + WorkDoneProgressParams + PartialResultParams +} +type DocumentColorRegistrationOptions struct { // line 2283 + TextDocumentRegistrationOptions + DocumentColorOptions + StaticRegistrationOptions +} + +/* + * Parameters of the document diagnostic request. + * + * @since 3.17.0 + */ +type DocumentDiagnosticParams struct { // line 3790 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The additional identifier provided during registration. + Identifier string `json:"identifier,omitempty"` + // The result id of a previous response if provided. + PreviousResultID string `json:"previousResultId,omitempty"` + WorkDoneProgressParams + PartialResultParams +} + +/* + * The result of a document diagnostic pull request. A report can + * either be a full report containing all diagnostics for the + * requested document or an unchanged report indicating that nothing + * has changed in terms of diagnostics in comparison to the last + * pull request. + * + * @since 3.17.0 + */ +type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 13909 +type DocumentDiagnosticReportKind string // line 12748 +/* + * A partial result for a document diagnostic report. + * + * @since 3.17.0 + */ +type DocumentDiagnosticReportPartialResult struct { // line 3833 + RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments"` +} + +/* + * A document filter describes a top level text document or + * a notebook cell document. + * + * @since 3.17.0 - proposed support for NotebookCellTextDocumentFilter. + */ +type DocumentFilter = Or_DocumentFilter // (alias) line 14118 +// Client capabilities of a [DocumentFormattingRequest](#DocumentFormattingRequest). +type DocumentFormattingClientCapabilities struct { // line 11915 + // Whether formatting supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +// Provider options for a [DocumentFormattingRequest](#DocumentFormattingRequest). +type DocumentFormattingOptions struct { // line 9241 + WorkDoneProgressOptions +} + +// The parameters of a [DocumentFormattingRequest](#DocumentFormattingRequest). +type DocumentFormattingParams struct { // line 5746 + // The document to format. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The format options. + Options FormattingOptions `json:"options"` + WorkDoneProgressParams +} + +// Registration options for a [DocumentFormattingRequest](#DocumentFormattingRequest). +type DocumentFormattingRegistrationOptions struct { // line 5774 + TextDocumentRegistrationOptions + DocumentFormattingOptions +} + +/* + * A document highlight is a range inside a text document which deserves + * special attention. Usually a document highlight is visualized by changing + * the background color of its range. + */ +type DocumentHighlight struct { // line 5140 + // The range this highlight applies to. + Range Range `json:"range"` + // The highlight kind, default is [text](#DocumentHighlightKind.Text). + Kind DocumentHighlightKind `json:"kind,omitempty"` +} + +// Client Capabilities for a [DocumentHighlightRequest](#DocumentHighlightRequest). +type DocumentHighlightClientCapabilities struct { // line 11650 + // Whether document highlight supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} +type DocumentHighlightKind uint32 // line 13327 +// Provider options for a [DocumentHighlightRequest](#DocumentHighlightRequest). +type DocumentHighlightOptions struct { // line 8975 + WorkDoneProgressOptions +} + +// Parameters for a [DocumentHighlightRequest](#DocumentHighlightRequest). +type DocumentHighlightParams struct { // line 5119 + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [DocumentHighlightRequest](#DocumentHighlightRequest). +type DocumentHighlightRegistrationOptions struct { // line 5163 + TextDocumentRegistrationOptions + DocumentHighlightOptions +} + +/* + * A document link is a range in a text document that links to an internal or external resource, like another + * text document or a web site. + */ +type DocumentLink struct { // line 5689 + // The range this link applies to. + Range Range `json:"range"` + // The uri this link points to. If missing a resolve request is sent later. + Target string `json:"target,omitempty"` + /* + * The tooltip text when you hover over this link. + * + * If a tooltip is provided, is will be displayed in a string that includes instructions on how to + * trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS, + * user settings, and localization. + * + * @since 3.15.0 + */ + Tooltip string `json:"tooltip,omitempty"` + /* + * A data entry field that is preserved on a document link between a + * DocumentLinkRequest and a DocumentLinkResolveRequest. + */ + Data interface{} `json:"data,omitempty"` +} + +// The client capabilities of a [DocumentLinkRequest](#DocumentLinkRequest). +type DocumentLinkClientCapabilities struct { // line 11876 + // Whether document link supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * Whether the client supports the `tooltip` property on `DocumentLink`. + * + * @since 3.15.0 + */ + TooltipSupport bool `json:"tooltipSupport,omitempty"` +} + +// Provider options for a [DocumentLinkRequest](#DocumentLinkRequest). +type DocumentLinkOptions struct { // line 9168 + // Document links have a resolve provider as well. + ResolveProvider bool `json:"resolveProvider,omitempty"` + WorkDoneProgressOptions +} + +// The parameters of a [DocumentLinkRequest](#DocumentLinkRequest). +type DocumentLinkParams struct { // line 5665 + // The document to provide document links for. + TextDocument TextDocumentIdentifier `json:"textDocument"` + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [DocumentLinkRequest](#DocumentLinkRequest). +type DocumentLinkRegistrationOptions struct { // line 5731 + TextDocumentRegistrationOptions + DocumentLinkOptions +} + +// Client capabilities of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). +type DocumentOnTypeFormattingClientCapabilities struct { // line 11945 + // Whether on type formatting supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +// Provider options for a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). +type DocumentOnTypeFormattingOptions struct { // line 9263 + // A character on which formatting should be triggered, like `{`. + FirstTriggerCharacter string `json:"firstTriggerCharacter"` + // More trigger characters. + MoreTriggerCharacter []string `json:"moreTriggerCharacter,omitempty"` +} + +// The parameters of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). +type DocumentOnTypeFormattingParams struct { // line 5840 + // The document to format. + TextDocument TextDocumentIdentifier `json:"textDocument"` + /* + * The position around which the on type formatting should happen. + * This is not necessarily the exact position where the character denoted + * by the property `ch` got typed. + */ + Position Position `json:"position"` + /* + * The character that has been typed that triggered the formatting + * on type request. That is not necessarily the last character that + * got inserted into the document since the client could auto insert + * characters as well (e.g. like automatic brace completion). + */ + Ch string `json:"ch"` + // The formatting options. + Options FormattingOptions `json:"options"` +} + +// Registration options for a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). +type DocumentOnTypeFormattingRegistrationOptions struct { // line 5878 + TextDocumentRegistrationOptions + DocumentOnTypeFormattingOptions +} + +// Client capabilities of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). +type DocumentRangeFormattingClientCapabilities struct { // line 11930 + // Whether range formatting supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +// Provider options for a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). +type DocumentRangeFormattingOptions struct { // line 9252 + WorkDoneProgressOptions +} + +// The parameters of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). +type DocumentRangeFormattingParams struct { // line 5789 + // The document to format. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The range to format + Range Range `json:"range"` + // The format options + Options FormattingOptions `json:"options"` + WorkDoneProgressParams +} + +// Registration options for a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). +type DocumentRangeFormattingRegistrationOptions struct { // line 5825 + TextDocumentRegistrationOptions + DocumentRangeFormattingOptions +} + +/* + * A document selector is the combination of one or many document filters. + * + * @sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**∕tsconfig.json' }]`; + * + * The use of a string as a document filter is deprecated @since 3.16.0. + */ +type DocumentSelector = []DocumentFilter // (alias) line 13990 +/* + * Represents programming constructs like variables, classes, interfaces etc. + * that appear in a document. Document symbols can be hierarchical and they + * have two ranges: one that encloses its definition and one that points to + * its most interesting range, e.g. the range of an identifier. + */ +type DocumentSymbol struct { // line 5231 + /* + * The name of this symbol. Will be displayed in the user interface and therefore must not be + * an empty string or a string only consisting of white spaces. + */ + Name string `json:"name"` + // More detail for this symbol, e.g the signature of a function. + Detail string `json:"detail,omitempty"` + // The kind of this symbol. + Kind SymbolKind `json:"kind"` + /* + * Tags for this document symbol. + * + * @since 3.16.0 + */ + Tags []SymbolTag `json:"tags,omitempty"` + /* + * Indicates if this symbol is deprecated. + * + * @deprecated Use tags instead + */ + Deprecated bool `json:"deprecated,omitempty"` + /* + * The range enclosing this symbol not including leading/trailing whitespace but everything else + * like comments. This information is typically used to determine if the clients cursor is + * inside the symbol to reveal in the symbol in the UI. + */ + Range Range `json:"range"` + /* + * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. + * Must be contained by the `range`. + */ + SelectionRange Range `json:"selectionRange"` + // Children of this symbol, e.g. properties of a class. + Children []DocumentSymbol `json:"children,omitempty"` +} + +// Client Capabilities for a [DocumentSymbolRequest](#DocumentSymbolRequest). +type DocumentSymbolClientCapabilities struct { // line 11665 + // Whether document symbol supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * Specific capabilities for the `SymbolKind` in the + * `textDocument/documentSymbol` request. + */ + SymbolKind *PSymbolKindPDocumentSymbol `json:"symbolKind,omitempty"` + // The client supports hierarchical document symbols. + HierarchicalDocumentSymbolSupport bool `json:"hierarchicalDocumentSymbolSupport,omitempty"` + /* + * The client supports tags on `SymbolInformation`. Tags are supported on + * `DocumentSymbol` if `hierarchicalDocumentSymbolSupport` is set to true. + * Clients supporting tags have to handle unknown tags gracefully. + * + * @since 3.16.0 + */ + TagSupport *PTagSupportPDocumentSymbol `json:"tagSupport,omitempty"` + /* + * The client supports an additional label presented in the UI when + * registering a document symbol provider. + * + * @since 3.16.0 + */ + LabelSupport bool `json:"labelSupport,omitempty"` +} + +// Provider options for a [DocumentSymbolRequest](#DocumentSymbolRequest). +type DocumentSymbolOptions struct { // line 9030 + /* + * A human-readable string that is shown when multiple outlines trees + * are shown for the same document. + * + * @since 3.16.0 + */ + Label string `json:"label,omitempty"` + WorkDoneProgressOptions +} + +// Parameters for a [DocumentSymbolRequest](#DocumentSymbolRequest). +type DocumentSymbolParams struct { // line 5178 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [DocumentSymbolRequest](#DocumentSymbolRequest). +type DocumentSymbolRegistrationOptions struct { // line 5312 + TextDocumentRegistrationOptions + DocumentSymbolOptions +} +type DocumentURI string // line 0 +type ErrorCodes int32 // line 12769 +// The client capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). +type ExecuteCommandClientCapabilities struct { // line 10988 + // Execute command supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +// The server capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). +type ExecuteCommandOptions struct { // line 9311 + // The commands to be executed on the server + Commands []string `json:"commands"` + WorkDoneProgressOptions +} + +// The parameters of a [ExecuteCommandRequest](#ExecuteCommandRequest). +type ExecuteCommandParams struct { // line 5960 + // The identifier of the actual command handler. + Command string `json:"command"` + // Arguments that the command should be invoked with. + Arguments []json.RawMessage `json:"arguments,omitempty"` + WorkDoneProgressParams +} + +// Registration options for a [ExecuteCommandRequest](#ExecuteCommandRequest). +type ExecuteCommandRegistrationOptions struct { // line 5992 + ExecuteCommandOptions +} +type ExecutionSummary struct { // line 10188 + /* + * A strict monotonically increasing value + * indicating the execution order of a cell + * inside a notebook. + */ + ExecutionOrder uint32 `json:"executionOrder"` + /* + * Whether the execution was successful or + * not if known by the client. + */ + Success bool `json:"success,omitempty"` +} + +// created for Literal +type FCellsPNotebookSelector struct { // line 9857 + Language string `json:"language"` +} + +// created for Literal +type FCodeActionKindPCodeActionLiteralSupport struct { // line 11768 + /* + * The code action kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + */ + ValueSet []CodeActionKind `json:"valueSet"` +} + +// created for Literal +type FEditRangePItemDefaults struct { // line 4797 + Insert Range `json:"insert"` + Replace Range `json:"replace"` +} + +// created for Literal +type FFullPRequests struct { // line 12230 + /* + * The client will send the `textDocument/semanticTokens/full/delta` request if + * the server provides a corresponding handler. + */ + Delta bool `json:"delta"` +} + +// created for Literal +type FInsertTextModeSupportPCompletionItem struct { // line 11321 + ValueSet []InsertTextMode `json:"valueSet"` +} + +// created for Literal +type FParameterInformationPSignatureInformation struct { // line 11487 + /* + * The client supports processing label offsets instead of a + * simple label string. + * + * @since 3.14.0 + */ + LabelOffsetSupport bool `json:"labelOffsetSupport"` +} + +// created for Literal +type FRangePRequests struct { // line 12210 +} + +// created for Literal +type FResolveSupportPCompletionItem struct { // line 11297 + // The properties that a client can resolve lazily. + Properties []string `json:"properties"` +} + +// created for Literal +type FStructurePCells struct { // line 7492 + // The change to the cell array. + Array NotebookCellArrayChange `json:"array"` + // Additional opened cell text documents. + DidOpen []TextDocumentItem `json:"didOpen"` + // Additional closed cell text documents. + DidClose []TextDocumentIdentifier `json:"didClose"` +} + +// created for Literal +type FTagSupportPCompletionItem struct { // line 11263 + // The tags supported by the client. + ValueSet []CompletionItemTag `json:"valueSet"` +} + +// created for Literal +type FTextContentPCells struct { // line 7550 + Document VersionedTextDocumentIdentifier `json:"document"` + Changes []TextDocumentContentChangeEvent `json:"changes"` +} +type FailureHandlingKind string // line 13719 +type FileChangeType uint32 // line 13480 +/* + * Represents information on a file/folder create. + * + * @since 3.16.0 + */ +type FileCreate struct { // line 6667 + // A file:// URI for the location of the file/folder being created. + URI string `json:"uri"` +} + +/* + * Represents information on a file/folder delete. + * + * @since 3.16.0 + */ +type FileDelete struct { // line 6916 + // A file:// URI for the location of the file/folder being deleted. + URI string `json:"uri"` +} + +// An event describing a file change. +type FileEvent struct { // line 8500 + // The file's uri. + URI DocumentURI `json:"uri"` + // The change type. + Type FileChangeType `json:"type"` +} + +/* + * Capabilities relating to events from file operations by the user in the client. + * + * These events do not come from the file system, they come from user operations + * like renaming a file in the UI. + * + * @since 3.16.0 + */ +type FileOperationClientCapabilities struct { // line 11035 + // Whether the client supports dynamic registration for file requests/notifications. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + // The client has support for sending didCreateFiles notifications. + DidCreate bool `json:"didCreate,omitempty"` + // The client has support for sending willCreateFiles requests. + WillCreate bool `json:"willCreate,omitempty"` + // The client has support for sending didRenameFiles notifications. + DidRename bool `json:"didRename,omitempty"` + // The client has support for sending willRenameFiles requests. + WillRename bool `json:"willRename,omitempty"` + // The client has support for sending didDeleteFiles notifications. + DidDelete bool `json:"didDelete,omitempty"` + // The client has support for sending willDeleteFiles requests. + WillDelete bool `json:"willDelete,omitempty"` +} + +/* + * A filter to describe in which file operation requests or notifications + * the server is interested in receiving. + * + * @since 3.16.0 + */ +type FileOperationFilter struct { // line 6869 + // A Uri scheme like `file` or `untitled`. + Scheme string `json:"scheme,omitempty"` + // The actual file operation pattern. + Pattern FileOperationPattern `json:"pattern"` +} + +/* + * Options for notifications/requests for user operations on files. + * + * @since 3.16.0 + */ +type FileOperationOptions struct { // line 9991 + // The server is interested in receiving didCreateFiles notifications. + DidCreate *FileOperationRegistrationOptions `json:"didCreate,omitempty"` + // The server is interested in receiving willCreateFiles requests. + WillCreate *FileOperationRegistrationOptions `json:"willCreate,omitempty"` + // The server is interested in receiving didRenameFiles notifications. + DidRename *FileOperationRegistrationOptions `json:"didRename,omitempty"` + // The server is interested in receiving willRenameFiles requests. + WillRename *FileOperationRegistrationOptions `json:"willRename,omitempty"` + // The server is interested in receiving didDeleteFiles file notifications. + DidDelete *FileOperationRegistrationOptions `json:"didDelete,omitempty"` + // The server is interested in receiving willDeleteFiles file requests. + WillDelete *FileOperationRegistrationOptions `json:"willDelete,omitempty"` +} + +/* + * A pattern to describe in which file operation requests or notifications + * the server is interested in receiving. + * + * @since 3.16.0 + */ +type FileOperationPattern struct { // line 9509 + /* + * The glob pattern to match. Glob patterns can have the following syntax: + * - `*` to match one or more characters in a path segment + * - `?` to match on one character in a path segment + * - `**` to match any number of path segments, including none + * - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) + */ + Glob string `json:"glob"` + /* + * Whether to match files or folders with this pattern. + * + * Matches both if undefined. + */ + Matches FileOperationPatternKind `json:"matches,omitempty"` + // Additional options used during matching. + Options *FileOperationPatternOptions `json:"options,omitempty"` +} +type FileOperationPatternKind string // line 13653 +/* + * Matching options for the file operation pattern. + * + * @since 3.16.0 + */ +type FileOperationPatternOptions struct { // line 10172 + // The pattern should be matched ignoring casing. + IgnoreCase bool `json:"ignoreCase,omitempty"` +} + +/* + * The options to register for file operations. + * + * @since 3.16.0 + */ +type FileOperationRegistrationOptions struct { // line 3286 + // The actual filters. + Filters []FileOperationFilter `json:"filters"` +} + +/* + * Represents information on a file/folder rename. + * + * @since 3.16.0 + */ +type FileRename struct { // line 6893 + // A file:// URI for the original location of the file/folder being renamed. + OldURI string `json:"oldUri"` + // A file:// URI for the new location of the file/folder being renamed. + NewURI string `json:"newUri"` +} +type FileSystemWatcher struct { // line 8522 + /* + * The glob pattern to watch. See {@link GlobPattern glob pattern} for more detail. + * + * @since 3.17.0 support for relative patterns. + */ + GlobPattern GlobPattern `json:"globPattern"` + /* + * The kind of events of interest. If omitted it defaults + * to WatchKind.Create | WatchKind.Change | WatchKind.Delete + * which is 7. + */ + Kind WatchKind `json:"kind,omitempty"` +} + +/* + * Represents a folding range. To be valid, start and end line must be bigger than zero and smaller + * than the number of lines in the document. Clients are free to ignore invalid ranges. + */ +type FoldingRange struct { // line 2437 + /* + * The zero-based start line of the range to fold. The folded area starts after the line's last character. + * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + */ + StartLine uint32 `json:"startLine"` + // The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. + StartCharacter uint32 `json:"startCharacter,omitempty"` + /* + * The zero-based end line of the range to fold. The folded area ends with the line's last character. + * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + */ + EndLine uint32 `json:"endLine"` + // The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. + EndCharacter uint32 `json:"endCharacter,omitempty"` + /* + * Describes the kind of the folding range such as `comment' or 'region'. The kind + * is used to categorize folding ranges and used by commands like 'Fold all comments'. + * See [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. + */ + Kind string `json:"kind,omitempty"` + /* + * The text that the client should show when the specified range is + * collapsed. If not defined or not supported by the client, a default + * will be chosen by the client. + * + * @since 3.17.0 + */ + CollapsedText string `json:"collapsedText,omitempty"` +} +type FoldingRangeClientCapabilities struct { // line 12004 + /* + * Whether implementation supports dynamic registration for folding range + * providers. If this is set to `true` the client supports the new + * `FoldingRangeRegistrationOptions` return value for the corresponding + * server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * The maximum number of folding ranges that the client prefers to receive + * per document. The value serves as a hint, servers are free to follow the + * limit. + */ + RangeLimit uint32 `json:"rangeLimit,omitempty"` + /* + * If set, the client signals that it only supports folding complete lines. + * If set, client will ignore specified `startCharacter` and `endCharacter` + * properties in a FoldingRange. + */ + LineFoldingOnly bool `json:"lineFoldingOnly,omitempty"` + /* + * Specific options for the folding range kind. + * + * @since 3.17.0 + */ + FoldingRangeKind *PFoldingRangeKindPFoldingRange `json:"foldingRangeKind,omitempty"` + /* + * Specific options for the folding range. + * + * @since 3.17.0 + */ + FoldingRange *PFoldingRangePFoldingRange `json:"foldingRange,omitempty"` +} +type FoldingRangeKind string // line 12841 +type FoldingRangeOptions struct { // line 6486 + WorkDoneProgressOptions +} + +// Parameters for a [FoldingRangeRequest](#FoldingRangeRequest). +type FoldingRangeParams struct { // line 2413 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + WorkDoneProgressParams + PartialResultParams +} +type FoldingRangeRegistrationOptions struct { // line 2496 + TextDocumentRegistrationOptions + FoldingRangeOptions + StaticRegistrationOptions +} + +// Value-object describing what options formatting should use. +type FormattingOptions struct { // line 9189 + // Size of a tab in spaces. + TabSize uint32 `json:"tabSize"` + // Prefer spaces over tabs. + InsertSpaces bool `json:"insertSpaces"` + /* + * Trim trailing whitespace on a line. + * + * @since 3.15.0 + */ + TrimTrailingWhitespace bool `json:"trimTrailingWhitespace,omitempty"` + /* + * Insert a newline character at the end of the file if one does not exist. + * + * @since 3.15.0 + */ + InsertFinalNewline bool `json:"insertFinalNewline,omitempty"` + /* + * Trim all newlines after the final newline at the end of the file. + * + * @since 3.15.0 + */ + TrimFinalNewlines bool `json:"trimFinalNewlines,omitempty"` +} + +/* + * A diagnostic report with a full set of problems. + * + * @since 3.17.0 + */ +type FullDocumentDiagnosticReport struct { // line 7240 + // A full document diagnostic report. + Kind string `json:"kind"` + /* + * An optional result id. If provided it will + * be sent on the next diagnostic request for the + * same document. + */ + ResultID string `json:"resultId,omitempty"` + // The actual items. + Items []Diagnostic `json:"items"` +} + +/* + * General client capabilities. + * + * @since 3.16.0 + */ +type GeneralClientCapabilities struct { // line 10690 + /* + * Client capability that signals how the client + * handles stale requests (e.g. a request + * for which the client will not process the response + * anymore since the information is outdated). + * + * @since 3.17.0 + */ + StaleRequestSupport *PStaleRequestSupportPGeneral `json:"staleRequestSupport,omitempty"` + /* + * Client capabilities specific to regular expressions. + * + * @since 3.16.0 + */ + RegularExpressions *RegularExpressionsClientCapabilities `json:"regularExpressions,omitempty"` + /* + * Client capabilities specific to the client's markdown parser. + * + * @since 3.16.0 + */ + Markdown *MarkdownClientCapabilities `json:"markdown,omitempty"` + /* + * The position encodings supported by the client. Client and server + * have to agree on the same position encoding to ensure that offsets + * (e.g. character position in a line) are interpreted the same on both + * sides. + * + * To keep the protocol backwards compatible the following applies: if + * the value 'utf-16' is missing from the array of position encodings + * servers can assume that the client supports UTF-16. UTF-16 is + * therefore a mandatory encoding. + * + * If omitted it defaults to ['utf-16']. + * + * Implementation considerations: since the conversion from one encoding + * into another requires the content of the file / line the conversion + * is best done where the file is read which is usually on the server + * side. + * + * @since 3.17.0 + */ + PositionEncodings []PositionEncodingKind `json:"positionEncodings,omitempty"` +} + +/* + * The glob pattern. Either a string pattern or a relative pattern. + * + * @since 3.17.0 + */ +type GlobPattern = string // (alias) line 14136 +// The result of a hover request. +type Hover struct { // line 4907 + // The hover's content + Contents MarkupContent `json:"contents"` + /* + * An optional range inside the text document that is used to + * visualize the hover, e.g. by changing the background color. + */ + Range Range `json:"range,omitempty"` +} +type HoverClientCapabilities struct { // line 11428 + // Whether hover supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * Client supports the following content formats for the content + * property. The order describes the preferred format of the client. + */ + ContentFormat []MarkupKind `json:"contentFormat,omitempty"` +} + +// Hover options. +type HoverOptions struct { // line 8796 + WorkDoneProgressOptions +} + +// Parameters for a [HoverRequest](#HoverRequest). +type HoverParams struct { // line 4890 + TextDocumentPositionParams + WorkDoneProgressParams +} + +// Registration options for a [HoverRequest](#HoverRequest). +type HoverRegistrationOptions struct { // line 4946 + TextDocumentRegistrationOptions + HoverOptions +} + +// @since 3.6.0 +type ImplementationClientCapabilities struct { // line 11609 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `ImplementationRegistrationOptions` return value + * for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * The client supports additional metadata in the form of definition links. + * + * @since 3.14.0 + */ + LinkSupport bool `json:"linkSupport,omitempty"` +} +type ImplementationOptions struct { // line 6338 + WorkDoneProgressOptions +} +type ImplementationParams struct { // line 2071 + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} +type ImplementationRegistrationOptions struct { // line 2111 + TextDocumentRegistrationOptions + ImplementationOptions + StaticRegistrationOptions +} + +/* + * The data type of the ResponseError if the + * initialize request fails. + */ +type InitializeError struct { // line 4148 + /* + * Indicates whether the client execute the following retry logic: + * (1) show the message provided by the ResponseError to the user + * (2) user selects retry or cancel + * (3) if user selected retry the initialize method is sent again. + */ + Retry bool `json:"retry"` +} +type InitializeParams struct { // line 4090 + XInitializeParams + WorkspaceFoldersInitializeParams +} + +// The result returned from an initialize request. +type InitializeResult struct { // line 4104 + // The capabilities the language server provides. + Capabilities ServerCapabilities `json:"capabilities"` + /* + * Information about the server. + * + * @since 3.15.0 + */ + ServerInfo PServerInfoMsg_initialize `json:"serverInfo,omitempty"` +} +type InitializedParams struct { // line 4162 +} + +/* + * Inlay hint information. + * + * @since 3.17.0 + */ +type InlayHint struct { // line 3667 + // The position of this hint. + Position *Position `json:"position"` + /* + * The label of this hint. A human readable string or an array of + * InlayHintLabelPart label parts. + * + * *Note* that neither the string nor the label part can be empty. + */ + Label []InlayHintLabelPart `json:"label"` + /* + * The kind of this hint. Can be omitted in which case the client + * should fall back to a reasonable default. + */ + Kind InlayHintKind `json:"kind,omitempty"` + /* + * Optional text edits that are performed when accepting this inlay hint. + * + * *Note* that edits are expected to change the document so that the inlay + * hint (or its nearest variant) is now part of the document and the inlay + * hint itself is now obsolete. + */ + TextEdits []TextEdit `json:"textEdits,omitempty"` + // The tooltip text when you hover over this item. + Tooltip *OrPTooltip_textDocument_inlayHint `json:"tooltip,omitempty"` + /* + * Render padding before the hint. + * + * Note: Padding should use the editor's background color, not the + * background color of the hint itself. That means padding can be used + * to visually align/separate an inlay hint. + */ + PaddingLeft bool `json:"paddingLeft,omitempty"` + /* + * Render padding after the hint. + * + * Note: Padding should use the editor's background color, not the + * background color of the hint itself. That means padding can be used + * to visually align/separate an inlay hint. + */ + PaddingRight bool `json:"paddingRight,omitempty"` + /* + * A data entry field that is preserved on an inlay hint between + * a `textDocument/inlayHint` and a `inlayHint/resolve` request. + */ + Data interface{} `json:"data,omitempty"` +} + +/* + * Inlay hint client capabilities. + * + * @since 3.17.0 + */ +type InlayHintClientCapabilities struct { // line 12395 + // Whether inlay hints support dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * Indicates which properties a client can resolve lazily on an inlay + * hint. + */ + ResolveSupport *PResolveSupportPInlayHint `json:"resolveSupport,omitempty"` +} +type InlayHintKind uint32 // line 13059 +/* + * An inlay hint label part allows for interactive and composite labels + * of inlay hints. + * + * @since 3.17.0 + */ +type InlayHintLabelPart struct { // line 7067 + // The value of this label part. + Value string `json:"value"` + /* + * The tooltip text when you hover over this label part. Depending on + * the client capability `inlayHint.resolveSupport` clients might resolve + * this property late using the resolve request. + */ + Tooltip *OrPTooltipPLabel `json:"tooltip,omitempty"` + /* + * An optional source code location that represents this + * label part. + * + * The editor will use this location for the hover and for code navigation + * features: This part will become a clickable link that resolves to the + * definition of the symbol at the given location (not necessarily the + * location itself), it shows the hover that shows at the given location, + * and it shows a context menu with further code navigation commands. + * + * Depending on the client capability `inlayHint.resolveSupport` clients + * might resolve this property late using the resolve request. + */ + Location *Location `json:"location,omitempty"` + /* + * An optional command for this label part. + * + * Depending on the client capability `inlayHint.resolveSupport` clients + * might resolve this property late using the resolve request. + */ + Command *Command `json:"command,omitempty"` +} + +/* + * Inlay hint options used during static registration. + * + * @since 3.17.0 + */ +type InlayHintOptions struct { // line 7140 + /* + * The server provides support to resolve additional + * information for an inlay hint item. + */ + ResolveProvider bool `json:"resolveProvider,omitempty"` + WorkDoneProgressOptions +} + +/* + * A parameter literal used in inlay hint requests. + * + * @since 3.17.0 + */ +type InlayHintParams struct { // line 3638 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The document range for which inlay hints should be computed. + Range Range `json:"range"` + WorkDoneProgressParams +} + +/* + * Inlay hint options used during static or dynamic registration. + * + * @since 3.17.0 + */ +type InlayHintRegistrationOptions struct { // line 3768 + InlayHintOptions + TextDocumentRegistrationOptions + StaticRegistrationOptions +} + +/* + * Client workspace capabilities specific to inlay hints. + * + * @since 3.17.0 + */ +type InlayHintWorkspaceClientCapabilities struct { // line 11121 + /* + * Whether the client implementation supports a refresh request sent from + * the server to the client. + * + * Note that this event is global and will force the client to refresh all + * inlay hints currently shown. It should be used with absolute care and + * is useful for situation where a server for example detects a project wide + * change that requires such a calculation. + */ + RefreshSupport bool `json:"refreshSupport,omitempty"` +} + +/* + * Inline value information can be provided by different means: + * - directly as a text value (class InlineValueText). + * - as a name to use for a variable lookup (class InlineValueVariableLookup) + * - as an evaluatable expression (class InlineValueEvaluatableExpression) + * The InlineValue types combines all inline value types into one type. + * + * @since 3.17.0 + */ +type InlineValue = Or_InlineValue // (alias) line 13887 +/* + * Client capabilities specific to inline values. + * + * @since 3.17.0 + */ +type InlineValueClientCapabilities struct { // line 12379 + // Whether implementation supports dynamic registration for inline value providers. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +// @since 3.17.0 +type InlineValueContext struct { // line 6953 + // The stack frame (as a DAP Id) where the execution has stopped. + FrameID int32 `json:"frameId"` + /* + * The document range where execution has stopped. + * Typically the end position of the range denotes the line where the inline values are shown. + */ + StoppedLocation Range `json:"stoppedLocation"` +} + +/* + * Provide an inline value through an expression evaluation. + * If only a range is specified, the expression will be extracted from the underlying document. + * An optional expression can be used to override the extracted expression. + * + * @since 3.17.0 + */ +type InlineValueEvaluatableExpression struct { // line 7031 + /* + * The document range for which the inline value applies. + * The range is used to extract the evaluatable expression from the underlying document. + */ + Range Range `json:"range"` + // If specified the expression overrides the extracted expression. + Expression string `json:"expression,omitempty"` +} + +/* + * Inline value options used during static registration. + * + * @since 3.17.0 + */ +type InlineValueOptions struct { // line 7055 + WorkDoneProgressOptions +} + +/* + * A parameter literal used in inline value requests. + * + * @since 3.17.0 + */ +type InlineValueParams struct { // line 3579 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The document range for which inline values should be computed. + Range Range `json:"range"` + /* + * Additional information about the context in which inline values were + * requested. + */ + Context InlineValueContext `json:"context"` + WorkDoneProgressParams +} + +/* + * Inline value options used during static or dynamic registration. + * + * @since 3.17.0 + */ +type InlineValueRegistrationOptions struct { // line 3616 + InlineValueOptions + TextDocumentRegistrationOptions + StaticRegistrationOptions +} + +/* + * Provide inline value as text. + * + * @since 3.17.0 + */ +type InlineValueText struct { // line 6976 + // The document range for which the inline value applies. + Range Range `json:"range"` + // The text of the inline value. + Text string `json:"text"` +} + +/* + * Provide inline value through a variable lookup. + * If only a range is specified, the variable name will be extracted from the underlying document. + * An optional variable name can be used to override the extracted name. + * + * @since 3.17.0 + */ +type InlineValueVariableLookup struct { // line 6999 + /* + * The document range for which the inline value applies. + * The range is used to extract the variable name from the underlying document. + */ + Range Range `json:"range"` + // If specified the name of the variable to look up. + VariableName string `json:"variableName,omitempty"` + // How to perform the lookup. + CaseSensitiveLookup bool `json:"caseSensitiveLookup"` +} + +/* + * Client workspace capabilities specific to inline values. + * + * @since 3.17.0 + */ +type InlineValueWorkspaceClientCapabilities struct { // line 11105 + /* + * Whether the client implementation supports a refresh request sent from the + * server to the client. + * + * Note that this event is global and will force the client to refresh all + * inline values currently shown. It should be used with absolute care and is + * useful for situation where a server for example detects a project wide + * change that requires such a calculation. + */ + RefreshSupport bool `json:"refreshSupport,omitempty"` +} + +/* + * A special text edit to provide an insert and a replace operation. + * + * @since 3.16.0 + */ +type InsertReplaceEdit struct { // line 8696 + // The string to be inserted. + NewText string `json:"newText"` + // The range if the insert is requested + Insert Range `json:"insert"` + // The range if the replace is requested. + Replace Range `json:"replace"` +} +type InsertTextFormat uint32 // line 13286 +type InsertTextMode uint32 // line 13306 +/* + * The LSP any type. + * Please note that strictly speaking a property with the value `undefined` + * can't be converted into JSON preserving the property name. However for + * convenience it is allowed and assumed that all these properties are + * optional as well. + * @since 3.17.0 + */ +type LSPAny = interface{} // (alias) line 13817 +/* + * LSP arrays. + * @since 3.17.0 + */ +type LSPArray = []interface{} // (alias) line 13805 +type LSPErrorCodes int32 // line 12809 +/* + * LSP object definition. + * @since 3.17.0 + */ +type LSPObject struct { // line 9618 +} + +/* + * Client capabilities for the linked editing range request. + * + * @since 3.16.0 + */ +type LinkedEditingRangeClientCapabilities struct { // line 12331 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + * return value for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} +type LinkedEditingRangeOptions struct { // line 6657 + WorkDoneProgressOptions +} +type LinkedEditingRangeParams struct { // line 3134 + TextDocumentPositionParams + WorkDoneProgressParams +} +type LinkedEditingRangeRegistrationOptions struct { // line 3177 + TextDocumentRegistrationOptions + LinkedEditingRangeOptions + StaticRegistrationOptions +} + +/* + * The result of a linked editing range request. + * + * @since 3.16.0 + */ +type LinkedEditingRanges struct { // line 3150 + /* + * A list of ranges that can be edited together. The ranges must have + * identical length and contain identical text content. The ranges cannot overlap. + */ + Ranges []Range `json:"ranges"` + /* + * An optional word pattern (regular expression) that describes valid contents for + * the given ranges. If no pattern is provided, the client configuration's word + * pattern will be used. + */ + WordPattern string `json:"wordPattern,omitempty"` +} + +/* + * Represents a location inside a resource, such as a line + * inside a text file. + */ +type Location struct { // line 2091 + URI DocumentURI `json:"uri"` + Range Range `json:"range"` +} + +/* + * Represents the connection of two locations. Provides additional metadata over normal [locations](#Location), + * including an origin range. + */ +type LocationLink struct { // line 6277 + /* + * Span of the origin of this link. + * + * Used as the underlined span for mouse interaction. Defaults to the word range at + * the definition position. + */ + OriginSelectionRange *Range `json:"originSelectionRange,omitempty"` + // The target resource identifier of this link. + TargetURI DocumentURI `json:"targetUri"` + /* + * The full target range of this link. If the target for example is a symbol then target range is the + * range enclosing this symbol not including leading/trailing whitespace but everything else + * like comments. This information is typically used to highlight the range in the editor. + */ + TargetRange Range `json:"targetRange"` + /* + * The range that should be selected and revealed when this link is being followed, e.g the name of a function. + * Must be contained by the `targetRange`. See also `DocumentSymbol#range` + */ + TargetSelectionRange Range `json:"targetSelectionRange"` +} + +// The log message parameters. +type LogMessageParams struct { // line 4273 + // The message type. See {@link MessageType} + Type MessageType `json:"type"` + // The actual message. + Message string `json:"message"` +} +type LogTraceParams struct { // line 6178 + Message string `json:"message"` + Verbose string `json:"verbose,omitempty"` +} + +/* + * Client capabilities specific to the used markdown parser. + * + * @since 3.16.0 + */ +type MarkdownClientCapabilities struct { // line 12550 + // The name of the parser. + Parser string `json:"parser"` + // The version of the parser. + Version string `json:"version,omitempty"` + /* + * A list of HTML tags that the client allows / supports in + * Markdown. + * + * @since 3.17.0 + */ + AllowedTags []string `json:"allowedTags,omitempty"` +} + +/* + * MarkedString can be used to render human readable text. It is either a markdown string + * or a code-block that provides a language and a code snippet. The language identifier + * is semantically equal to the optional language identifier in fenced code blocks in GitHub + * issues. See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting + * + * The pair of a language and a value is an equivalent to markdown: + * ```${language} + * ${value} + * ``` + * + * Note that markdown strings will be sanitized - that means html will be escaped. + * @deprecated use MarkupContent instead. + */ +type MarkedString = Or_MarkedString // (alias) line 14084 +/* + * A `MarkupContent` literal represents a string value which content is interpreted base on its + * kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. + * + * If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. + * See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting + * + * Here is an example how such a string can be constructed using JavaScript / TypeScript: + * ```ts + * let markdown: MarkdownContent = { + * kind: MarkupKind.Markdown, + * value: [ + * '# Header', + * 'Some text', + * '```typescript', + * 'someCode();', + * '```' + * ].join('\ + * ') + * }; + * ``` + * + * *Please Note* that clients might sanitize the return markdown. A client could decide to + * remove HTML from the markdown to avoid script execution. + */ +type MarkupContent struct { // line 7118 + // The type of the Markup + Kind MarkupKind `json:"kind"` + // The content itself + Value string `json:"value"` +} +type MarkupKind string // line 13433 +type MessageActionItem struct { // line 4260 + // A short title like 'Retry', 'Open Log' etc. + Title string `json:"title"` +} +type MessageType uint32 // line 13080 +/* + * Moniker definition to match LSIF 0.5 moniker definition. + * + * @since 3.16.0 + */ +type Moniker struct { // line 3360 + // The scheme of the moniker. For example tsc or .Net + Scheme string `json:"scheme"` + /* + * The identifier of the moniker. The value is opaque in LSIF however + * schema owners are allowed to define the structure if they want. + */ + Identifier string `json:"identifier"` + // The scope in which the moniker is unique + Unique UniquenessLevel `json:"unique"` + // The moniker kind if known. + Kind MonikerKind `json:"kind,omitempty"` +} + +/* + * Client capabilities specific to the moniker request. + * + * @since 3.16.0 + */ +type MonikerClientCapabilities struct { // line 12347 + /* + * Whether moniker supports dynamic registration. If this is set to `true` + * the client supports the new `MonikerRegistrationOptions` return value + * for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} +type MonikerKind string // line 13033 +type MonikerOptions struct { // line 6931 + WorkDoneProgressOptions +} +type MonikerParams struct { // line 3340 + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} +type MonikerRegistrationOptions struct { // line 3400 + TextDocumentRegistrationOptions + MonikerOptions +} + +// created for Literal +type Msg_MarkedString struct { // line 14093 + Language string `json:"language"` + Value string `json:"value"` +} + +// created for Literal +type Msg_NotebookDocumentFilter struct { // line 14268 + // The type of the enclosing notebook. + NotebookType string `json:"notebookType"` + // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + Scheme string `json:"scheme"` + // A glob pattern. + Pattern string `json:"pattern"` +} + +// created for Literal +type Msg_PrepareRename2Gn struct { // line 13936 + Range Range `json:"range"` + Placeholder string `json:"placeholder"` +} + +// created for Literal +type Msg_TextDocumentContentChangeEvent struct { // line 14033 + // The range of the document that changed. + Range *Range `json:"range"` + /* + * The optional length of the range that got replaced. + * + * @deprecated use range instead. + */ + RangeLength uint32 `json:"rangeLength"` + // The new text for the provided range. + Text string `json:"text"` +} + +// created for Literal +type Msg_TextDocumentFilter struct { // line 14159 + // A language id, like `typescript`. + Language string `json:"language"` + // A Uri [scheme](#Uri.scheme), like `file` or `untitled`. + Scheme string `json:"scheme"` + // A glob pattern, like `*.{ts,js}`. + Pattern string `json:"pattern"` +} + +// created for Literal +type Msg_XInitializeParams_clientInfo struct { // line 7678 + // The name of the client as defined by the client. + Name string `json:"name"` + // The client's version as defined by the client. + Version string `json:"version"` +} + +/* + * A notebook cell. + * + * A cell's document URI must be unique across ALL notebook + * cells and can therefore be used to uniquely identify a + * notebook cell or the cell's text document. + * + * @since 3.17.0 + */ +type NotebookCell struct { // line 9624 + // The cell's kind + Kind NotebookCellKind `json:"kind"` + /* + * The URI of the cell's text document + * content. + */ + Document DocumentURI `json:"document"` + /* + * Additional metadata stored with the cell. + * + * Note: should always be an object literal (e.g. LSPObject) + */ + Metadata *LSPObject `json:"metadata,omitempty"` + /* + * Additional execution summary information + * if supported by the client. + */ + ExecutionSummary *ExecutionSummary `json:"executionSummary,omitempty"` +} + +/* + * A change describing how to move a `NotebookCell` + * array from state S to S'. + * + * @since 3.17.0 + */ +type NotebookCellArrayChange struct { // line 9665 + // The start oftest of the cell that changed. + Start uint32 `json:"start"` + // The deleted cells + DeleteCount uint32 `json:"deleteCount"` + // The new cells, if any + Cells []NotebookCell `json:"cells,omitempty"` +} +type NotebookCellKind uint32 // line 13674 +/* + * A notebook cell text document filter denotes a cell text + * document by different properties. + * + * @since 3.17.0 + */ +type NotebookCellTextDocumentFilter struct { // line 10139 + /* + * A filter that matches against the notebook + * containing the notebook cell. If a string + * value is provided it matches against the + * notebook type. '*' matches every notebook. + */ + Notebook NotebookDocumentFilter `json:"notebook"` + /* + * A language id like `python`. + * + * Will be matched against the language id of the + * notebook cell document. '*' matches every language. + */ + Language string `json:"language,omitempty"` +} + +/* + * A notebook document. + * + * @since 3.17.0 + */ +type NotebookDocument struct { // line 7359 + // The notebook document's uri. + URI URI `json:"uri"` + // The type of the notebook. + NotebookType string `json:"notebookType"` + /* + * The version number of this document (it will increase after each + * change, including undo/redo). + */ + Version int32 `json:"version"` + /* + * Additional metadata stored with the notebook + * document. + * + * Note: should always be an object literal (e.g. LSPObject) + */ + Metadata *LSPObject `json:"metadata,omitempty"` + // The cells of a notebook. + Cells []NotebookCell `json:"cells"` +} + +/* + * A change event for a notebook document. + * + * @since 3.17.0 + */ +type NotebookDocumentChangeEvent struct { // line 7471 + /* + * The changed meta data if any. + * + * Note: should always be an object literal (e.g. LSPObject) + */ + Metadata *LSPObject `json:"metadata,omitempty"` + // Changes to cells + Cells *PCellsPChange `json:"cells,omitempty"` +} + +/* + * Capabilities specific to the notebook document support. + * + * @since 3.17.0 + */ +type NotebookDocumentClientCapabilities struct { // line 10639 + /* + * Capabilities specific to notebook document synchronization + * + * @since 3.17.0 + */ + Synchronization NotebookDocumentSyncClientCapabilities `json:"synchronization"` +} + +/* + * A notebook document filter denotes a notebook document by + * different properties. The properties will be match + * against the notebook's URI (same as with documents) + * + * @since 3.17.0 + */ +type NotebookDocumentFilter = Msg_NotebookDocumentFilter // (alias) line 14263 +/* + * A literal to identify a notebook document in the client. + * + * @since 3.17.0 + */ +type NotebookDocumentIdentifier struct { // line 7587 + // The notebook document's uri. + URI URI `json:"uri"` +} + +/* + * Notebook specific client capabilities. + * + * @since 3.17.0 + */ +type NotebookDocumentSyncClientCapabilities struct { // line 12459 + /* + * Whether implementation supports dynamic registration. If this is + * set to `true` the client supports the new + * `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + * return value for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + // The client supports sending execution summary data per cell. + ExecutionSummarySupport bool `json:"executionSummarySupport,omitempty"` +} + +/* + * Options specific to a notebook plus its cells + * to be synced to the server. + * + * If a selector provides a notebook document + * filter but no cell selector all cells of a + * matching notebook document will be synced. + * + * If a selector provides no notebook document + * filter but only a cell selector all notebook + * document that contain at least one matching + * cell will be synced. + * + * @since 3.17.0 + */ +type NotebookDocumentSyncOptions struct { // line 9821 + // The notebooks to be synced + NotebookSelector []PNotebookSelectorPNotebookDocumentSync `json:"notebookSelector"` + /* + * Whether save notification should be forwarded to + * the server. Will only be honored if mode === `notebook`. + */ + Save bool `json:"save,omitempty"` +} + +/* + * Registration options specific to a notebook. + * + * @since 3.17.0 + */ +type NotebookDocumentSyncRegistrationOptions struct { // line 9941 + NotebookDocumentSyncOptions + StaticRegistrationOptions +} + +// A text document identifier to optionally denote a specific version of a text document. +type OptionalVersionedTextDocumentIdentifier struct { // line 9363 + /* + * The version number of this document. If a versioned text document identifier + * is sent from the server to the client and the file is not open in the editor + * (the server has not received an open notification before) the server can send + * `null` to indicate that the version is unknown and the content on disk is the + * truth (as specified with document content ownership). + */ + Version int32 `json:"version"` + TextDocumentIdentifier +} + +// created for Or [Range FEditRangePItemDefaults] +type OrFEditRangePItemDefaults struct { // line 4791 + Value interface{} `json:"value"` +} + +// created for Or [string NotebookDocumentFilter] +type OrFNotebookPNotebookSelector struct { // line 9838 + Value interface{} `json:"value"` +} + +// created for Or [Location PLocationMsg_workspace_symbol] +type OrPLocation_workspace_symbol struct { // line 5540 + Value interface{} `json:"value"` +} + +// created for Or [string []string] +type OrPSection_workspace_didChangeConfiguration struct { // line 4186 + Value interface{} `json:"value"` +} + +// created for Or [string MarkupContent] +type OrPTooltipPLabel struct { // line 7081 + Value interface{} `json:"value"` +} + +// created for Or [string MarkupContent] +type OrPTooltip_textDocument_inlayHint struct { // line 3722 + Value interface{} `json:"value"` +} + +// created for Or [Location []Location] +type Or_Definition struct { // line 13780 + Value interface{} `json:"value"` +} + +// created for Or [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport] +type Or_DocumentDiagnosticReport struct { // line 13912 + Value interface{} `json:"value"` +} + +// created for Or [TextDocumentFilter NotebookCellTextDocumentFilter] +type Or_DocumentFilter struct { // line 14121 + Value interface{} `json:"value"` +} + +// created for Or [InlineValueText InlineValueVariableLookup InlineValueEvaluatableExpression] +type Or_InlineValue struct { // line 13890 + Value interface{} `json:"value"` +} + +// created for Or [string Msg_MarkedString] +type Or_MarkedString struct { // line 14087 + Value interface{} `json:"value"` +} + +// created for Or [WorkspaceFolder URI] +type Or_RelativePattern_baseUri struct { // line 10768 + Value interface{} `json:"value"` +} + +// created for Or [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport] +type Or_WorkspaceDocumentDiagnosticReport struct { // line 14013 + Value interface{} `json:"value"` +} + +// created for Or [Declaration []DeclarationLink ] +type Or_textDocument_declaration struct { // line 257 + Value interface{} `json:"value"` +} + +// created for Literal +type PCellsPChange struct { // line 7486 + /* + * Changes to the cell structure to add or + * remove cells. + */ + Structure FStructurePCells `json:"structure"` + /* + * Changes to notebook cells properties like its + * kind, execution summary or metadata. + */ + Data []NotebookCell `json:"data"` + // Changes to the text content of notebook cells. + TextContent []FTextContentPCells `json:"textContent"` +} + +// created for Literal +type PChangeAnnotationSupportPWorkspaceEdit struct { // line 10842 + /* + * Whether the client groups edits with equal labels into tree nodes, + * for instance all edits labelled with \"Changes in Strings\" would + * be a tree node. + */ + GroupsOnLabel bool `json:"groupsOnLabel"` +} + +// created for Literal +type PCodeActionLiteralSupportPCodeAction struct { // line 11762 + /* + * The code action kind is support with the following value + * set. + */ + CodeActionKind FCodeActionKindPCodeActionLiteralSupport `json:"codeActionKind"` +} + +// created for Literal +type PCompletionItemKindPCompletion struct { // line 11360 + /* + * The completion item kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + * + * If this property is not present the client only supports + * the completion items kinds from `Text` to `Reference` as defined in + * the initial version of the protocol. + */ + ValueSet []CompletionItemKind `json:"valueSet"` +} + +// created for Literal +type PCompletionItemPCompletion struct { // line 11209 + /* + * Client supports snippets as insert text. + * + * A snippet can define tab stops and placeholders with `$1`, `$2` + * and `${3:foo}`. `$0` defines the final tab stop, it defaults to + * the end of the snippet. Placeholders with equal identifiers are linked, + * that is typing in one will update others too. + */ + SnippetSupport bool `json:"snippetSupport"` + // Client supports commit characters on a completion item. + CommitCharactersSupport bool `json:"commitCharactersSupport"` + /* + * Client supports the following content formats for the documentation + * property. The order describes the preferred format of the client. + */ + DocumentationFormat []MarkupKind `json:"documentationFormat"` + // Client supports the deprecated property on a completion item. + DeprecatedSupport bool `json:"deprecatedSupport"` + // Client supports the preselect property on a completion item. + PreselectSupport bool `json:"preselectSupport"` + /* + * Client supports the tag property on a completion item. Clients supporting + * tags have to handle unknown tags gracefully. Clients especially need to + * preserve unknown tags when sending a completion item back to the server in + * a resolve call. + * + * @since 3.15.0 + */ + TagSupport FTagSupportPCompletionItem `json:"tagSupport"` + /* + * Client support insert replace edit to control different behavior if a + * completion item is inserted in the text or should replace text. + * + * @since 3.16.0 + */ + InsertReplaceSupport bool `json:"insertReplaceSupport"` + /* + * Indicates which properties a client can resolve lazily on a completion + * item. Before version 3.16.0 only the predefined properties `documentation` + * and `details` could be resolved lazily. + * + * @since 3.16.0 + */ + ResolveSupport FResolveSupportPCompletionItem `json:"resolveSupport"` + /* + * The client supports the `insertTextMode` property on + * a completion item to override the whitespace handling mode + * as defined by the client (see `insertTextMode`). + * + * @since 3.16.0 + */ + InsertTextModeSupport FInsertTextModeSupportPCompletionItem `json:"insertTextModeSupport"` + /* + * The client has support for completion item label + * details (see also `CompletionItemLabelDetails`). + * + * @since 3.17.0 + */ + LabelDetailsSupport bool `json:"labelDetailsSupport"` +} + +// created for Literal +type PCompletionItemPCompletionProvider struct { // line 8767 + /* + * The server has support for completion item label + * details (see also `CompletionItemLabelDetails`) when + * receiving a completion item in a resolve call. + * + * @since 3.17.0 + */ + LabelDetailsSupport bool `json:"labelDetailsSupport"` +} + +// created for Literal +type PCompletionListPCompletion struct { // line 11402 + /* + * The client supports the following itemDefaults on + * a completion list. + * + * The value lists the supported property names of the + * `CompletionList.itemDefaults` object. If omitted + * no properties are supported. + * + * @since 3.17.0 + */ + ItemDefaults []string `json:"itemDefaults"` +} + +// created for Literal +type PDisabledMsg_textDocument_codeAction struct { // line 5446 + /* + * Human readable description of why the code action is currently disabled. + * + * This is displayed in the code actions UI. + */ + Reason string `json:"reason"` +} + +// created for Literal +type PFoldingRangeKindPFoldingRange struct { // line 12037 + /* + * The folding range kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + */ + ValueSet []FoldingRangeKind `json:"valueSet"` +} + +// created for Literal +type PFoldingRangePFoldingRange struct { // line 12062 + /* + * If set, the client signals that it supports setting collapsedText on + * folding ranges to display custom labels instead of the default text. + * + * @since 3.17.0 + */ + CollapsedText bool `json:"collapsedText"` +} + +// created for Literal +type PFullESemanticTokensOptions struct { // line 6591 + // The server supports deltas for full documents. + Delta bool `json:"delta"` +} + +// created for Literal +type PItemDefaultsMsg_textDocument_completion struct { // line 4772 + /* + * A default commit character set. + * + * @since 3.17.0 + */ + CommitCharacters []string `json:"commitCharacters"` + /* + * A default edit range. + * + * @since 3.17.0 + */ + EditRange OrFEditRangePItemDefaults `json:"editRange"` + /* + * A default insert text format. + * + * @since 3.17.0 + */ + InsertTextFormat InsertTextFormat `json:"insertTextFormat"` + /* + * A default insert text mode. + * + * @since 3.17.0 + */ + InsertTextMode InsertTextMode `json:"insertTextMode"` + /* + * A default data value. + * + * @since 3.17.0 + */ + Data interface{} `json:"data"` +} + +// created for Literal +type PLocationMsg_workspace_symbol struct { // line 5546 + URI DocumentURI `json:"uri"` +} + +// created for Literal +type PMessageActionItemPShowMessage struct { // line 12490 + /* + * Whether the client supports additional attributes which + * are preserved and send back to the server in the + * request's response. + */ + AdditionalPropertiesSupport bool `json:"additionalPropertiesSupport"` +} + +// created for Literal +type PNotebookSelectorPNotebookDocumentSync struct { // line 9831 + /* + * The notebook to be synced If a string + * value is provided it matches against the + * notebook type. '*' matches every notebook. + */ + Notebook OrFNotebookPNotebookSelector `json:"notebook"` + // The cells of the matching notebook to be synced. + Cells []FCellsPNotebookSelector `json:"cells"` +} + +// created for Literal +type PRangeESemanticTokensOptions struct { // line 6571 +} + +// created for Literal +type PRequestsPSemanticTokens struct { // line 12198 + /* + * The client will send the `textDocument/semanticTokens/range` request if + * the server provides a corresponding handler. + */ + Range bool `json:"range"` + /* + * The client will send the `textDocument/semanticTokens/full` request if + * the server provides a corresponding handler. + */ + Full interface{} `json:"full"` +} + +// created for Literal +type PResolveSupportPCodeAction struct { // line 11827 + // The properties that a client can resolve lazily. + Properties []string `json:"properties"` +} + +// created for Literal +type PResolveSupportPInlayHint struct { // line 12410 + // The properties that a client can resolve lazily. + Properties []string `json:"properties"` +} + +// created for Literal +type PResolveSupportPSymbol struct { // line 10964 + /* + * The properties that a client can resolve lazily. Usually + * `location.range` + */ + Properties []string `json:"properties"` +} + +// created for Literal +type PServerInfoMsg_initialize struct { // line 4118 + // The name of the server as defined by the server. + Name string `json:"name"` + // The server's version as defined by the server. + Version string `json:"version"` +} + +// created for Literal +type PSignatureInformationPSignatureHelp struct { // line 11469 + /* + * Client supports the following content formats for the documentation + * property. The order describes the preferred format of the client. + */ + DocumentationFormat []MarkupKind `json:"documentationFormat"` + // Client capabilities specific to parameter information. + ParameterInformation FParameterInformationPSignatureInformation `json:"parameterInformation"` + /* + * The client supports the `activeParameter` property on `SignatureInformation` + * literal. + * + * @since 3.16.0 + */ + ActiveParameterSupport bool `json:"activeParameterSupport"` +} + +// created for Literal +type PStaleRequestSupportPGeneral struct { // line 10696 + // The client will actively cancel the request. + Cancel bool `json:"cancel"` + /* + * The list of requests for which the client + * will retry the request if it receives a + * response with error code `ContentModified` + */ + RetryOnContentModified []string `json:"retryOnContentModified"` +} + +// created for Literal +type PSymbolKindPDocumentSymbol struct { // line 11680 + /* + * The symbol kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + * + * If this property is not present the client only supports + * the symbol kinds from `File` to `Array` as defined in + * the initial version of the protocol. + */ + ValueSet []SymbolKind `json:"valueSet"` +} + +// created for Literal +type PSymbolKindPSymbol struct { // line 10916 + /* + * The symbol kind values the client supports. When this + * property exists the client also guarantees that it will + * handle values outside its set gracefully and falls back + * to a default value when unknown. + * + * If this property is not present the client only supports + * the symbol kinds from `File` to `Array` as defined in + * the initial version of the protocol. + */ + ValueSet []SymbolKind `json:"valueSet"` +} + +// created for Literal +type PTagSupportPDocumentSymbol struct { // line 11713 + // The tags supported by the client. + ValueSet []SymbolTag `json:"valueSet"` +} + +// created for Literal +type PTagSupportPPublishDiagnostics struct { // line 12113 + // The tags supported by the client. + ValueSet []DiagnosticTag `json:"valueSet"` +} + +// created for Literal +type PTagSupportPSymbol struct { // line 10940 + // The tags supported by the client. + ValueSet []SymbolTag `json:"valueSet"` +} + +// The parameters of a configuration request. +type ParamConfiguration struct { // line 2207 + Items []ConfigurationItem `json:"items"` +} +type ParamInitialize struct { // line 4090 + XInitializeParams + WorkspaceFoldersInitializeParams +} + +/* + * Represents a parameter of a callable-signature. A parameter can + * have a label and a doc-comment. + */ +type ParameterInformation struct { // line 10089 + /* + * The label of this parameter information. + * + * Either a string or an inclusive start and exclusive end offsets within its containing + * signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 + * string representation as `Position` and `Range` does. + * + * *Note*: a label of type string should be a substring of its containing signature label. + * Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. + */ + Label string `json:"label"` + /* + * The human-readable doc-comment of this parameter. Will be shown + * in the UI but can be omitted. + */ + Documentation string `json:"documentation,omitempty"` +} +type PartialResultParams struct { // line 2223 + /* + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + */ + PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` +} + +/* + * The glob pattern to watch relative to the base path. Glob patterns can have the following syntax: + * - `*` to match one or more characters in a path segment + * - `?` to match on one character in a path segment + * - `**` to match any number of path segments, including none + * - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) + * + * @since 3.17.0 + */ +type Pattern = string // (alias) line 14372 +/* + * Position in a text document expressed as zero-based line and character + * offset. Prior to 3.17 the offsets were always based on a UTF-16 string + * representation. So a string of the form `a𐐀b` the character offset of the + * character `a` is 0, the character offset of `𐐀` is 1 and the character + * offset of b is 3 since `𐐀` is represented using two code units in UTF-16. + * Since 3.17 clients and servers can agree on a different string encoding + * representation (e.g. UTF-8). The client announces it's supported encoding + * via the client capability [`general.positionEncodings`](#clientCapabilities). + * The value is an array of position encodings the client supports, with + * decreasing preference (e.g. the encoding at index `0` is the most preferred + * one). To stay backwards compatible the only mandatory encoding is UTF-16 + * represented via the string `utf-16`. The server can pick one of the + * encodings offered by the client and signals that encoding back to the + * client via the initialize result's property + * [`capabilities.positionEncoding`](#serverCapabilities). If the string value + * `utf-16` is missing from the client's capability `general.positionEncodings` + * servers can safely assume that the client supports UTF-16. If the server + * omits the position encoding in its initialize result the encoding defaults + * to the string value `utf-16`. Implementation considerations: since the + * conversion from one encoding into another requires the content of the + * file / line the conversion is best done where the file is read which is + * usually on the server side. + * + * Positions are line end character agnostic. So you can not specify a position + * that denotes `\\r|\ + * ` or `\ + * |` where `|` represents the character offset. + * + * @since 3.17.0 - support for negotiated position encoding. + */ +type Position struct { // line 6506 + /* + * Line position in a document (zero-based). + * + * If a line number is greater than the number of lines in a document, it defaults back to the number of lines in the document. + * If a line number is negative, it defaults to 0. + */ + Line uint32 `json:"line"` + /* + * Character offset on a line in a document (zero-based). + * + * The meaning of this offset is determined by the negotiated + * `PositionEncodingKind`. + * + * If the character value is greater than the line length it defaults back to the + * line length. + */ + Character uint32 `json:"character"` +} +type PositionEncodingKind string // line 13453 +type PrepareRename2Gn = Msg_PrepareRename2Gn // (alias) line 13927 +type PrepareRenameParams struct { // line 5944 + TextDocumentPositionParams + WorkDoneProgressParams +} +type PrepareRenameResult = Msg_PrepareRename2Gn // (alias) line 13927 +type PrepareSupportDefaultBehavior interface{} // line 13748 +/* + * A previous result id in a workspace pull request. + * + * @since 3.17.0 + */ +type PreviousResultID struct { // line 7336 + /* + * The URI for which the client knowns a + * result id. + */ + URI DocumentURI `json:"uri"` + // The value of the previous result id. + Value string `json:"value"` +} + +/* + * A previous result id in a workspace pull request. + * + * @since 3.17.0 + */ +type PreviousResultId struct { // line 7336 + /* + * The URI for which the client knowns a + * result id. + */ + URI DocumentURI `json:"uri"` + // The value of the previous result id. + Value string `json:"value"` +} +type ProgressParams struct { // line 6220 + // The progress token provided by the client or server. + Token ProgressToken `json:"token"` + // The progress data. + Value interface{} `json:"value"` +} +type ProgressToken = interface{} // (alias) line 13974 +// The publish diagnostic client capabilities. +type PublishDiagnosticsClientCapabilities struct { // line 12098 + // Whether the clients accepts diagnostics with related information. + RelatedInformation bool `json:"relatedInformation,omitempty"` + /* + * Client supports the tag property to provide meta data about a diagnostic. + * Clients supporting tags have to handle unknown tags gracefully. + * + * @since 3.15.0 + */ + TagSupport *PTagSupportPPublishDiagnostics `json:"tagSupport,omitempty"` + /* + * Whether the client interprets the version property of the + * `textDocument/publishDiagnostics` notification's parameter. + * + * @since 3.15.0 + */ + VersionSupport bool `json:"versionSupport,omitempty"` + /* + * Client supports a codeDescription property + * + * @since 3.16.0 + */ + CodeDescriptionSupport bool `json:"codeDescriptionSupport,omitempty"` + /* + * Whether code action supports the `data` property which is + * preserved between a `textDocument/publishDiagnostics` and + * `textDocument/codeAction` request. + * + * @since 3.16.0 + */ + DataSupport bool `json:"dataSupport,omitempty"` +} + +// The publish diagnostic notification's parameters. +type PublishDiagnosticsParams struct { // line 4484 + // The URI for which diagnostic information is reported. + URI DocumentURI `json:"uri"` + /* + * Optional the version number of the document the diagnostics are published for. + * + * @since 3.15.0 + */ + Version int32 `json:"version,omitempty"` + // An array of diagnostic information items. + Diagnostics []Diagnostic `json:"diagnostics"` +} + +/* + * A range in a text document expressed as (zero-based) start and end positions. + * + * If you want to specify a range that contains a line including the line ending + * character(s) then use an end position denoting the start of the next line. + * For example: + * ```ts + * { + * start: { line: 5, character: 23 } + * end : { line 6, character : 0 } + * } + * ``` + */ +type Range struct { // line 6316 + // The range's start position. + Start Position `json:"start"` + // The range's end position. + End Position `json:"end"` +} + +// Client Capabilities for a [ReferencesRequest](#ReferencesRequest). +type ReferenceClientCapabilities struct { // line 11635 + // Whether references supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +/* + * Value-object that contains additional information when + * requesting references. + */ +type ReferenceContext struct { // line 8950 + // Include the declaration of the current symbol. + IncludeDeclaration bool `json:"includeDeclaration"` +} + +// Reference options. +type ReferenceOptions struct { // line 8964 + WorkDoneProgressOptions +} + +// Parameters for a [ReferencesRequest](#ReferencesRequest). +type ReferenceParams struct { // line 5075 + Context ReferenceContext `json:"context"` + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [ReferencesRequest](#ReferencesRequest). +type ReferenceRegistrationOptions struct { // line 5104 + TextDocumentRegistrationOptions + ReferenceOptions +} + +// General parameters to to register for an notification or to register a provider. +type Registration struct { // line 7602 + /* + * The id used to register the request. The id can be used to deregister + * the request again. + */ + ID string `json:"id"` + // The method / capability to register for. + Method string `json:"method"` + // Options necessary for the registration. + RegisterOptions interface{} `json:"registerOptions,omitempty"` +} +type RegistrationParams struct { // line 4060 + Registrations []Registration `json:"registrations"` +} + +/* + * Client capabilities specific to regular expressions. + * + * @since 3.16.0 + */ +type RegularExpressionsClientCapabilities struct { // line 12526 + // The engine's name. + Engine string `json:"engine"` + // The engine's version. + Version string `json:"version,omitempty"` +} + +/* + * A full diagnostic report with a set of related documents. + * + * @since 3.17.0 + */ +type RelatedFullDocumentDiagnosticReport struct { // line 7162 + /* + * Diagnostics of related documents. This information is useful + * in programming languages where code in a file A can generate + * diagnostics in a file B which A depends on. An example of + * such a language is C/C++ where marco definitions in a file + * a.cpp and result in errors in a header file b.hpp. + * + * @since 3.17.0 + */ + RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments,omitempty"` + FullDocumentDiagnosticReport +} + +/* + * An unchanged diagnostic report with a set of related documents. + * + * @since 3.17.0 + */ +type RelatedUnchangedDocumentDiagnosticReport struct { // line 7201 + /* + * Diagnostics of related documents. This information is useful + * in programming languages where code in a file A can generate + * diagnostics in a file B which A depends on. An example of + * such a language is C/C++ where marco definitions in a file + * a.cpp and result in errors in a header file b.hpp. + * + * @since 3.17.0 + */ + RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments,omitempty"` + UnchangedDocumentDiagnosticReport +} + +/* + * A relative pattern is a helper to construct glob patterns that are matched + * relatively to a base URI. The common value for a `baseUri` is a workspace + * folder root, but it can be another absolute URI as well. + * + * @since 3.17.0 + */ +type RelativePattern struct { // line 10762 + /* + * A workspace folder or a base URI to which this pattern will be matched + * against relatively. + */ + BaseURI Or_RelativePattern_baseUri `json:"baseUri"` + // The actual glob pattern; + Pattern Pattern `json:"pattern"` +} +type RenameClientCapabilities struct { // line 11960 + // Whether rename supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * Client supports testing for validity of rename operations + * before execution. + * + * @since 3.12.0 + */ + PrepareSupport bool `json:"prepareSupport,omitempty"` + /* + * Client supports the default behavior result. + * + * The value indicates the default behavior used by the + * client. + * + * @since 3.16.0 + */ + PrepareSupportDefaultBehavior interface{} `json:"prepareSupportDefaultBehavior,omitempty"` + /* + * Whether the client honors the change annotations in + * text edits and resource operations returned via the + * rename request's workspace edit by for example presenting + * the workspace edit in the user interface and asking + * for confirmation. + * + * @since 3.16.0 + */ + HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` +} + +// Rename file operation +type RenameFile struct { // line 6754 + // A rename + Kind string `json:"kind"` + // The old (existing) location. + OldURI DocumentURI `json:"oldUri"` + // The new location. + NewURI DocumentURI `json:"newUri"` + // Rename options. + Options *RenameFileOptions `json:"options,omitempty"` + ResourceOperation +} + +// Rename file options +type RenameFileOptions struct { // line 9461 + // Overwrite target if existing. Overwrite wins over `ignoreIfExists` + Overwrite bool `json:"overwrite,omitempty"` + // Ignores if target exists. + IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` +} + +/* + * The parameters sent in notifications/requests for user-initiated renames of + * files. + * + * @since 3.16.0 + */ +type RenameFilesParams struct { // line 3304 + /* + * An array of all files/folders renamed in this operation. When a folder is renamed, only + * the folder will be included, and not its children. + */ + Files []FileRename `json:"files"` +} + +// Provider options for a [RenameRequest](#RenameRequest). +type RenameOptions struct { // line 9289 + /* + * Renames should be checked and tested before being executed. + * + * @since version 3.12.0 + */ + PrepareProvider bool `json:"prepareProvider,omitempty"` + WorkDoneProgressOptions +} + +// The parameters of a [RenameRequest](#RenameRequest). +type RenameParams struct { // line 5893 + // The document to rename. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The position at which this request was sent. + Position Position `json:"position"` + /* + * The new name of the symbol. If the given name is not valid the + * request must return a [ResponseError](#ResponseError) with an + * appropriate message set. + */ + NewName string `json:"newName"` + WorkDoneProgressParams +} + +// Registration options for a [RenameRequest](#RenameRequest). +type RenameRegistrationOptions struct { // line 5929 + TextDocumentRegistrationOptions + RenameOptions +} + +// A generic resource operation. +type ResourceOperation struct { // line 9413 + // The resource operation kind. + Kind string `json:"kind"` + /* + * An optional annotation identifier describing the operation. + * + * @since 3.16.0 + */ + AnnotationID ChangeAnnotationIdentifier `json:"annotationId,omitempty"` +} +type ResourceOperationKind string // line 13695 +// Save options. +type SaveOptions struct { // line 8485 + // The client is supposed to include the content on save. + IncludeText bool `json:"includeText,omitempty"` +} + +/* + * A selection range represents a part of a selection hierarchy. A selection range + * may have a parent selection range that contains it. + */ +type SelectionRange struct { // line 2591 + // The [range](#Range) of this selection range. + Range Range `json:"range"` + // The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. + Parent *SelectionRange `json:"parent,omitempty"` +} +type SelectionRangeClientCapabilities struct { // line 12084 + /* + * Whether implementation supports dynamic registration for selection range providers. If this is set to `true` + * the client supports the new `SelectionRangeRegistrationOptions` return value for the corresponding server + * capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} +type SelectionRangeOptions struct { // line 6529 + WorkDoneProgressOptions +} + +// A parameter literal used in selection range requests. +type SelectionRangeParams struct { // line 2556 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The positions inside the text document. + Positions []Position `json:"positions"` + WorkDoneProgressParams + PartialResultParams +} +type SelectionRangeRegistrationOptions struct { // line 2614 + SelectionRangeOptions + TextDocumentRegistrationOptions + StaticRegistrationOptions +} +type SemanticTokenModifiers string // line 12696 +type SemanticTokenTypes string // line 12589 +// @since 3.16.0 +type SemanticTokens struct { // line 2902 + /* + * An optional result id. If provided and clients support delta updating + * the client will include the result id in the next semantic token request. + * A server can then instead of computing all semantic tokens again simply + * send a delta. + */ + ResultID string `json:"resultId,omitempty"` + // The actual tokens. + Data []uint32 `json:"data"` +} + +// @since 3.16.0 +type SemanticTokensClientCapabilities struct { // line 12183 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + * return value for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * Which requests the client supports and might send to the server + * depending on the server's capability. Please note that clients might not + * show semantic tokens or degrade some of the user experience if a range + * or full request is advertised by the client but not provided by the + * server. If for example the client capability `requests.full` and + * `request.range` are both set to true but the server only provides a + * range provider the client might not render a minimap correctly or might + * even decide to not show any semantic tokens at all. + */ + Requests PRequestsPSemanticTokens `json:"requests"` + // The token types that the client supports. + TokenTypes []string `json:"tokenTypes"` + // The token modifiers that the client supports. + TokenModifiers []string `json:"tokenModifiers"` + // The token formats the clients supports. + Formats []string `json:"formats"` + // Whether the client supports tokens that can overlap each other. + OverlappingTokenSupport bool `json:"overlappingTokenSupport,omitempty"` + // Whether the client supports tokens that can span multiple lines. + MultilineTokenSupport bool `json:"multilineTokenSupport,omitempty"` + /* + * Whether the client allows the server to actively cancel a + * semantic token request, e.g. supports returning + * LSPErrorCodes.ServerCancelled. If a server does the client + * needs to retrigger the request. + * + * @since 3.17.0 + */ + ServerCancelSupport bool `json:"serverCancelSupport,omitempty"` + /* + * Whether the client uses semantic tokens to augment existing + * syntax tokens. If set to `true` client side created syntax + * tokens and semantic tokens are both used for colorization. If + * set to `false` the client only uses the returned semantic tokens + * for colorization. + * + * If the value is `undefined` then the client behavior is not + * specified. + * + * @since 3.17.0 + */ + AugmentsSyntaxTokens bool `json:"augmentsSyntaxTokens,omitempty"` +} + +// @since 3.16.0 +type SemanticTokensDelta struct { // line 3001 + ResultID string `json:"resultId,omitempty"` + // The semantic token edits to transform a previous result into a new result. + Edits []SemanticTokensEdit `json:"edits"` +} + +// @since 3.16.0 +type SemanticTokensDeltaParams struct { // line 2968 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + /* + * The result id of a previous response. The result Id can either point to a full response + * or a delta response depending on what was received last. + */ + PreviousResultID string `json:"previousResultId"` + WorkDoneProgressParams + PartialResultParams +} + +// @since 3.16.0 +type SemanticTokensDeltaPartialResult struct { // line 3027 + Edits []SemanticTokensEdit `json:"edits"` +} + +// @since 3.16.0 +type SemanticTokensEdit struct { // line 6622 + // The start offset of the edit. + Start uint32 `json:"start"` + // The count of elements to remove. + DeleteCount uint32 `json:"deleteCount"` + // The elements to insert. + Data []uint32 `json:"data,omitempty"` +} + +// @since 3.16.0 +type SemanticTokensLegend struct { // line 9334 + // The token types a server uses. + TokenTypes []string `json:"tokenTypes"` + // The token modifiers a server uses. + TokenModifiers []string `json:"tokenModifiers"` +} + +// @since 3.16.0 +type SemanticTokensOptions struct { // line 6551 + // The legend used by the server + Legend SemanticTokensLegend `json:"legend"` + /* + * Server supports providing semantic tokens for a specific range + * of a document. + */ + Range interface{} `json:"range,omitempty"` + // Server supports providing semantic tokens for a full document. + Full bool `json:"full,omitempty"` + WorkDoneProgressOptions +} + +// @since 3.16.0 +type SemanticTokensParams struct { // line 2877 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + WorkDoneProgressParams + PartialResultParams +} + +// @since 3.16.0 +type SemanticTokensPartialResult struct { // line 2929 + Data []uint32 `json:"data"` +} + +// @since 3.16.0 +type SemanticTokensRangeParams struct { // line 3044 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The range the semantic tokens are requested for. + Range Range `json:"range"` + WorkDoneProgressParams + PartialResultParams +} + +// @since 3.16.0 +type SemanticTokensRegistrationOptions struct { // line 2946 + TextDocumentRegistrationOptions + SemanticTokensOptions + StaticRegistrationOptions +} + +// @since 3.16.0 +type SemanticTokensWorkspaceClientCapabilities struct { // line 11003 + /* + * Whether the client implementation supports a refresh request sent from + * the server to the client. + * + * Note that this event is global and will force the client to refresh all + * semantic tokens currently shown. It should be used with absolute care + * and is useful for situation where a server for example detects a project + * wide change that requires such a calculation. + */ + RefreshSupport bool `json:"refreshSupport,omitempty"` +} + +/* + * Defines the capabilities provided by a language + * server. + */ +type ServerCapabilities struct { // line 7829 + /* + * The position encoding the server picked from the encodings offered + * by the client via the client capability `general.positionEncodings`. + * + * If the client didn't provide any position encodings the only valid + * value that a server can return is 'utf-16'. + * + * If omitted it defaults to 'utf-16'. + * + * @since 3.17.0 + */ + PositionEncoding PositionEncodingKind `json:"positionEncoding,omitempty"` + /* + * Defines how text documents are synced. Is either a detailed structure + * defining each notification or for backwards compatibility the + * TextDocumentSyncKind number. + */ + TextDocumentSync interface{} `json:"textDocumentSync,omitempty"` + /* + * Defines how notebook documents are synced. + * + * @since 3.17.0 + */ + NotebookDocumentSync interface{} `json:"notebookDocumentSync,omitempty"` + // The server provides completion support. + CompletionProvider CompletionOptions `json:"completionProvider,omitempty"` + // The server provides hover support. + HoverProvider bool `json:"hoverProvider,omitempty"` + // The server provides signature help support. + SignatureHelpProvider SignatureHelpOptions `json:"signatureHelpProvider,omitempty"` + // The server provides Goto Declaration support. + DeclarationProvider bool `json:"declarationProvider,omitempty"` + // The server provides goto definition support. + DefinitionProvider bool `json:"definitionProvider,omitempty"` + // The server provides Goto Type Definition support. + TypeDefinitionProvider interface{} `json:"typeDefinitionProvider,omitempty"` + // The server provides Goto Implementation support. + ImplementationProvider interface{} `json:"implementationProvider,omitempty"` + // The server provides find references support. + ReferencesProvider bool `json:"referencesProvider,omitempty"` + // The server provides document highlight support. + DocumentHighlightProvider bool `json:"documentHighlightProvider,omitempty"` + // The server provides document symbol support. + DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"` + /* + * The server provides code actions. CodeActionOptions may only be + * specified if the client states that it supports + * `codeActionLiteralSupport` in its initial `initialize` request. + */ + CodeActionProvider interface{} `json:"codeActionProvider,omitempty"` + // The server provides code lens. + CodeLensProvider *CodeLensOptions `json:"codeLensProvider,omitempty"` + // The server provides document link support. + DocumentLinkProvider DocumentLinkOptions `json:"documentLinkProvider,omitempty"` + // The server provides color provider support. + ColorProvider interface{} `json:"colorProvider,omitempty"` + // The server provides workspace symbol support. + WorkspaceSymbolProvider bool `json:"workspaceSymbolProvider,omitempty"` + // The server provides document formatting. + DocumentFormattingProvider bool `json:"documentFormattingProvider,omitempty"` + // The server provides document range formatting. + DocumentRangeFormattingProvider bool `json:"documentRangeFormattingProvider,omitempty"` + // The server provides document formatting on typing. + DocumentOnTypeFormattingProvider *DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"` + /* + * The server provides rename support. RenameOptions may only be + * specified if the client states that it supports + * `prepareSupport` in its initial `initialize` request. + */ + RenameProvider interface{} `json:"renameProvider,omitempty"` + // The server provides folding provider support. + FoldingRangeProvider interface{} `json:"foldingRangeProvider,omitempty"` + // The server provides selection range support. + SelectionRangeProvider interface{} `json:"selectionRangeProvider,omitempty"` + // The server provides execute command support. + ExecuteCommandProvider ExecuteCommandOptions `json:"executeCommandProvider,omitempty"` + /* + * The server provides call hierarchy support. + * + * @since 3.16.0 + */ + CallHierarchyProvider interface{} `json:"callHierarchyProvider,omitempty"` + /* + * The server provides linked editing range support. + * + * @since 3.16.0 + */ + LinkedEditingRangeProvider interface{} `json:"linkedEditingRangeProvider,omitempty"` + /* + * The server provides semantic tokens support. + * + * @since 3.16.0 + */ + SemanticTokensProvider interface{} `json:"semanticTokensProvider,omitempty"` + /* + * The server provides moniker support. + * + * @since 3.16.0 + */ + MonikerProvider interface{} `json:"monikerProvider,omitempty"` + /* + * The server provides type hierarchy support. + * + * @since 3.17.0 + */ + TypeHierarchyProvider interface{} `json:"typeHierarchyProvider,omitempty"` + /* + * The server provides inline values. + * + * @since 3.17.0 + */ + InlineValueProvider interface{} `json:"inlineValueProvider,omitempty"` + /* + * The server provides inlay hints. + * + * @since 3.17.0 + */ + InlayHintProvider interface{} `json:"inlayHintProvider,omitempty"` + /* + * The server has support for pull model diagnostics. + * + * @since 3.17.0 + */ + DiagnosticProvider interface{} `json:"diagnosticProvider,omitempty"` + // Workspace specific server capabilities. + Workspace Workspace6Gn `json:"workspace,omitempty"` + // Experimental server capabilities. + Experimental interface{} `json:"experimental,omitempty"` +} +type SetTraceParams struct { // line 6166 + Value TraceValues `json:"value"` +} + +/* + * Client capabilities for the showDocument request. + * + * @since 3.16.0 + */ +type ShowDocumentClientCapabilities struct { // line 12511 + /* + * The client has support for the showDocument + * request. + */ + Support bool `json:"support"` +} + +/* + * Params to show a document. + * + * @since 3.16.0 + */ +type ShowDocumentParams struct { // line 3077 + // The document uri to show. + URI URI `json:"uri"` + /* + * Indicates to show the resource in an external program. + * To show for example `https://code.visualstudio.com/` + * in the default WEB browser set `external` to `true`. + */ + External bool `json:"external,omitempty"` + /* + * An optional property to indicate whether the editor + * showing the document should take focus or not. + * Clients might ignore this property if an external + * program is started. + */ + TakeFocus bool `json:"takeFocus,omitempty"` + /* + * An optional selection range if the document is a text + * document. Clients might ignore the property if an + * external program is started or the file is not a text + * file. + */ + Selection *Range `json:"selection,omitempty"` +} + +/* + * The result of a showDocument request. + * + * @since 3.16.0 + */ +type ShowDocumentResult struct { // line 3119 + // A boolean indicating if the show was successful. + Success bool `json:"success"` +} + +// The parameters of a notification message. +type ShowMessageParams struct { // line 4205 + // The message type. See {@link MessageType} + Type MessageType `json:"type"` + // The actual message. + Message string `json:"message"` +} + +// Show message request client capabilities +type ShowMessageRequestClientCapabilities struct { // line 12484 + // Capabilities specific to the `MessageActionItem` type. + MessageActionItem *PMessageActionItemPShowMessage `json:"messageActionItem,omitempty"` +} +type ShowMessageRequestParams struct { // line 4227 + // The message type. See {@link MessageType} + Type MessageType `json:"type"` + // The actual message. + Message string `json:"message"` + // The message action items to present. + Actions []MessageActionItem `json:"actions,omitempty"` +} + +/* + * Signature help represents the signature of something + * callable. There can be multiple signature but only one + * active and only one active parameter. + */ +type SignatureHelp struct { // line 4989 + // One or more signatures. + Signatures []SignatureInformation `json:"signatures"` + /* + * The active signature. If omitted or the value lies outside the + * range of `signatures` the value defaults to zero or is ignored if + * the `SignatureHelp` has no signatures. + * + * Whenever possible implementors should make an active decision about + * the active signature and shouldn't rely on a default value. + * + * In future version of the protocol this property might become + * mandatory to better express this. + */ + ActiveSignature uint32 `json:"activeSignature,omitempty"` + /* + * The active parameter of the active signature. If omitted or the value + * lies outside the range of `signatures[activeSignature].parameters` + * defaults to 0 if the active signature has parameters. If + * the active signature has no parameters it is ignored. + * In future version of the protocol this property might become + * mandatory to better express the active parameter if the + * active signature does have any. + */ + ActiveParameter uint32 `json:"activeParameter,omitempty"` +} + +// Client Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). +type SignatureHelpClientCapabilities struct { // line 11454 + // Whether signature help supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * The client supports the following `SignatureInformation` + * specific properties. + */ + SignatureInformation *PSignatureInformationPSignatureHelp `json:"signatureInformation,omitempty"` + /* + * The client supports to send additional context information for a + * `textDocument/signatureHelp` request. A client that opts into + * contextSupport will also support the `retriggerCharacters` on + * `SignatureHelpOptions`. + * + * @since 3.15.0 + */ + ContextSupport bool `json:"contextSupport,omitempty"` +} + +/* + * Additional information about the context in which a signature help request was triggered. + * + * @since 3.15.0 + */ +type SignatureHelpContext struct { // line 8807 + // Action that caused signature help to be triggered. + TriggerKind SignatureHelpTriggerKind `json:"triggerKind"` + /* + * Character that caused signature help to be triggered. + * + * This is undefined when `triggerKind !== SignatureHelpTriggerKind.TriggerCharacter` + */ + TriggerCharacter string `json:"triggerCharacter,omitempty"` + /* + * `true` if signature help was already showing when it was triggered. + * + * Retriggers occurs when the signature help is already active and can be caused by actions such as + * typing a trigger character, a cursor move, or document content changes. + */ + IsRetrigger bool `json:"isRetrigger"` + /* + * The currently active `SignatureHelp`. + * + * The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on + * the user navigating through available signatures. + */ + ActiveSignatureHelp *SignatureHelp `json:"activeSignatureHelp,omitempty"` +} + +// Server Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). +type SignatureHelpOptions struct { // line 8902 + // List of characters that trigger signature help automatically. + TriggerCharacters []string `json:"triggerCharacters,omitempty"` + /* + * List of characters that re-trigger signature help. + * + * These trigger characters are only active when signature help is already showing. All trigger characters + * are also counted as re-trigger characters. + * + * @since 3.15.0 + */ + RetriggerCharacters []string `json:"retriggerCharacters,omitempty"` + WorkDoneProgressOptions +} + +// Parameters for a [SignatureHelpRequest](#SignatureHelpRequest). +type SignatureHelpParams struct { // line 4961 + /* + * The signature help context. This is only available if the client specifies + * to send this using the client capability `textDocument.signatureHelp.contextSupport === true` + * + * @since 3.15.0 + */ + Context *SignatureHelpContext `json:"context,omitempty"` + TextDocumentPositionParams + WorkDoneProgressParams +} + +// Registration options for a [SignatureHelpRequest](#SignatureHelpRequest). +type SignatureHelpRegistrationOptions struct { // line 5024 + TextDocumentRegistrationOptions + SignatureHelpOptions +} +type SignatureHelpTriggerKind uint32 // line 13606 +/* + * Represents the signature of something callable. A signature + * can have a label, like a function-name, a doc-comment, and + * a set of parameters. + */ +type SignatureInformation struct { // line 8848 + /* + * The label of this signature. Will be shown in + * the UI. + */ + Label string `json:"label"` + /* + * The human-readable doc-comment of this signature. Will be shown + * in the UI but can be omitted. + */ + Documentation string `json:"documentation,omitempty"` + // The parameters of this signature. + Parameters []ParameterInformation `json:"parameters,omitempty"` + /* + * The index of the active parameter. + * + * If provided, this is used in place of `SignatureHelp.activeParameter`. + * + * @since 3.16.0 + */ + ActiveParameter uint32 `json:"activeParameter,omitempty"` +} + +/* + * Static registration options to be returned in the initialize + * request. + */ +type StaticRegistrationOptions struct { // line 6348 + /* + * The id used to register the request. The id can be used to deregister + * the request again. See also Registration#id. + */ + ID string `json:"id,omitempty"` +} + +/* + * Represents information about programming constructs like variables, classes, + * interfaces etc. + */ +type SymbolInformation struct { // line 5202 + /* + * Indicates if this symbol is deprecated. + * + * @deprecated Use tags instead + */ + Deprecated bool `json:"deprecated,omitempty"` + /* + * The location of this symbol. The location's range is used by a tool + * to reveal the location in the editor. If the symbol is selected in the + * tool the range's start information is used to position the cursor. So + * the range usually spans more than the actual symbol's name and does + * normally include things like visibility modifiers. + * + * The range doesn't have to denote a node range in the sense of an abstract + * syntax tree. It can therefore not be used to re-construct a hierarchy of + * the symbols. + */ + Location Location `json:"location"` + // The name of this symbol. + Name string `json:"name"` + // The kind of this symbol. + Kind SymbolKind `json:"kind"` + /* + * Tags for this symbol. + * + * @since 3.16.0 + */ + Tags []SymbolTag `json:"tags,omitempty"` + /* + * The name of the symbol containing this symbol. This information is for + * user interface purposes (e.g. to render a qualifier in the user interface + * if necessary). It can't be used to re-infer a hierarchy for the document + * symbols. + */ + ContainerName string `json:"containerName,omitempty"` +} +type SymbolKind uint32 // line 12867 +type SymbolTag uint32 // line 12981 +// Describe options to be used when registered for text document change events. +type TextDocumentChangeRegistrationOptions struct { // line 4334 + // How documents are synced to the server. + SyncKind TextDocumentSyncKind `json:"syncKind"` + TextDocumentRegistrationOptions +} + +// Text document specific client capabilities. +type TextDocumentClientCapabilities struct { // line 10349 + // Defines which synchronization capabilities the client supports. + Synchronization *TextDocumentSyncClientCapabilities `json:"synchronization,omitempty"` + // Capabilities specific to the `textDocument/completion` request. + Completion CompletionClientCapabilities `json:"completion,omitempty"` + // Capabilities specific to the `textDocument/hover` request. + Hover HoverClientCapabilities `json:"hover,omitempty"` + // Capabilities specific to the `textDocument/signatureHelp` request. + SignatureHelp *SignatureHelpClientCapabilities `json:"signatureHelp,omitempty"` + /* + * Capabilities specific to the `textDocument/declaration` request. + * + * @since 3.14.0 + */ + Declaration *DeclarationClientCapabilities `json:"declaration,omitempty"` + // Capabilities specific to the `textDocument/definition` request. + Definition *DefinitionClientCapabilities `json:"definition,omitempty"` + /* + * Capabilities specific to the `textDocument/typeDefinition` request. + * + * @since 3.6.0 + */ + TypeDefinition *TypeDefinitionClientCapabilities `json:"typeDefinition,omitempty"` + /* + * Capabilities specific to the `textDocument/implementation` request. + * + * @since 3.6.0 + */ + Implementation *ImplementationClientCapabilities `json:"implementation,omitempty"` + // Capabilities specific to the `textDocument/references` request. + References *ReferenceClientCapabilities `json:"references,omitempty"` + // Capabilities specific to the `textDocument/documentHighlight` request. + DocumentHighlight *DocumentHighlightClientCapabilities `json:"documentHighlight,omitempty"` + // Capabilities specific to the `textDocument/documentSymbol` request. + DocumentSymbol DocumentSymbolClientCapabilities `json:"documentSymbol,omitempty"` + // Capabilities specific to the `textDocument/codeAction` request. + CodeAction CodeActionClientCapabilities `json:"codeAction,omitempty"` + // Capabilities specific to the `textDocument/codeLens` request. + CodeLens *CodeLensClientCapabilities `json:"codeLens,omitempty"` + // Capabilities specific to the `textDocument/documentLink` request. + DocumentLink *DocumentLinkClientCapabilities `json:"documentLink,omitempty"` + /* + * Capabilities specific to the `textDocument/documentColor` and the + * `textDocument/colorPresentation` request. + * + * @since 3.6.0 + */ + ColorProvider *DocumentColorClientCapabilities `json:"colorProvider,omitempty"` + // Capabilities specific to the `textDocument/formatting` request. + Formatting *DocumentFormattingClientCapabilities `json:"formatting,omitempty"` + // Capabilities specific to the `textDocument/rangeFormatting` request. + RangeFormatting *DocumentRangeFormattingClientCapabilities `json:"rangeFormatting,omitempty"` + // Capabilities specific to the `textDocument/onTypeFormatting` request. + OnTypeFormatting *DocumentOnTypeFormattingClientCapabilities `json:"onTypeFormatting,omitempty"` + // Capabilities specific to the `textDocument/rename` request. + Rename RenameClientCapabilities `json:"rename,omitempty"` + /* + * Capabilities specific to the `textDocument/foldingRange` request. + * + * @since 3.10.0 + */ + FoldingRange FoldingRangeClientCapabilities `json:"foldingRange,omitempty"` + /* + * Capabilities specific to the `textDocument/selectionRange` request. + * + * @since 3.15.0 + */ + SelectionRange *SelectionRangeClientCapabilities `json:"selectionRange,omitempty"` + // Capabilities specific to the `textDocument/publishDiagnostics` notification. + PublishDiagnostics PublishDiagnosticsClientCapabilities `json:"publishDiagnostics,omitempty"` + /* + * Capabilities specific to the various call hierarchy requests. + * + * @since 3.16.0 + */ + CallHierarchy *CallHierarchyClientCapabilities `json:"callHierarchy,omitempty"` + /* + * Capabilities specific to the various semantic token request. + * + * @since 3.16.0 + */ + SemanticTokens SemanticTokensClientCapabilities `json:"semanticTokens,omitempty"` + /* + * Capabilities specific to the `textDocument/linkedEditingRange` request. + * + * @since 3.16.0 + */ + LinkedEditingRange *LinkedEditingRangeClientCapabilities `json:"linkedEditingRange,omitempty"` + /* + * Client capabilities specific to the `textDocument/moniker` request. + * + * @since 3.16.0 + */ + Moniker *MonikerClientCapabilities `json:"moniker,omitempty"` + /* + * Capabilities specific to the various type hierarchy requests. + * + * @since 3.17.0 + */ + TypeHierarchy *TypeHierarchyClientCapabilities `json:"typeHierarchy,omitempty"` + /* + * Capabilities specific to the `textDocument/inlineValue` request. + * + * @since 3.17.0 + */ + InlineValue *InlineValueClientCapabilities `json:"inlineValue,omitempty"` + /* + * Capabilities specific to the `textDocument/inlayHint` request. + * + * @since 3.17.0 + */ + InlayHint *InlayHintClientCapabilities `json:"inlayHint,omitempty"` + /* + * Capabilities specific to the diagnostic pull model. + * + * @since 3.17.0 + */ + Diagnostic *DiagnosticClientCapabilities `json:"diagnostic,omitempty"` +} + +/* + * An event describing a change to a text document. If only a text is provided + * it is considered to be the full content of the document. + */ +type TextDocumentContentChangeEvent = Msg_TextDocumentContentChangeEvent // (alias) line 14028 +/* + * Describes textual changes on a text document. A TextDocumentEdit describes all changes + * on a document version Si and after they are applied move the document to version Si+1. + * So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any + * kind of ordering. However the edits must be non overlapping. + */ +type TextDocumentEdit struct { // line 6682 + // The text document to change. + TextDocument OptionalVersionedTextDocumentIdentifier `json:"textDocument"` + /* + * The edits to be applied. + * + * @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a + * client capability. + */ + Edits []TextEdit `json:"edits"` +} + +/* + * A document filter denotes a document by different properties like + * the [language](#TextDocument.languageId), the [scheme](#Uri.scheme) of + * its resource, or a glob-pattern that is applied to the [path](#TextDocument.fileName). + * + * Glob patterns can have the following syntax: + * - `*` to match one or more characters in a path segment + * - `?` to match on one character in a path segment + * - `**` to match any number of path segments, including none + * - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) + * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) + * + * @sample A language filter that applies to typescript files on disk: `{ language: 'typescript', scheme: 'file' }` + * @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**package.json' }` + * + * @since 3.17.0 + */ +type TextDocumentFilter = Msg_TextDocumentFilter // (alias) line 14154 +// A literal to identify a text document in the client. +type TextDocumentIdentifier struct { // line 6424 + // The text document's uri. + URI DocumentURI `json:"uri"` +} + +/* + * An item to transfer a text document from the client to the + * server. + */ +type TextDocumentItem struct { // line 7410 + // The text document's uri. + URI DocumentURI `json:"uri"` + // The text document's language identifier. + LanguageID string `json:"languageId"` + /* + * The version number of this document (it will increase after each + * change, including undo/redo). + */ + Version int32 `json:"version"` + // The content of the opened text document. + Text string `json:"text"` +} + +/* + * A parameter literal used in requests to pass a text document and a position inside that + * document. + */ +type TextDocumentPositionParams struct { // line 6241 + // The text document. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The position inside the text document. + Position Position `json:"position"` +} + +// General text document registration options. +type TextDocumentRegistrationOptions struct { // line 2390 + /* + * A document selector to identify the scope of the registration. If set to null + * the document selector provided on the client side will be used. + */ + DocumentSelector DocumentSelector `json:"documentSelector"` +} +type TextDocumentSaveReason uint32 // line 13135 +// Save registration options. +type TextDocumentSaveRegistrationOptions struct { // line 4391 + TextDocumentRegistrationOptions + SaveOptions +} +type TextDocumentSyncClientCapabilities struct { // line 11153 + // Whether text document synchronization supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + // The client supports sending will save notifications. + WillSave bool `json:"willSave,omitempty"` + /* + * The client supports sending a will save request and + * waits for a response providing text edits which will + * be applied to the document before it is saved. + */ + WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` + // The client supports did save notifications. + DidSave bool `json:"didSave,omitempty"` +} +type TextDocumentSyncKind uint32 // line 13110 +type TextDocumentSyncOptions struct { // line 9762 + /* + * Open and close notifications are sent to the server. If omitted open close notification should not + * be sent. + */ + OpenClose bool `json:"openClose,omitempty"` + /* + * Change notifications are sent to the server. See TextDocumentSyncKind.None, TextDocumentSyncKind.Full + * and TextDocumentSyncKind.Incremental. If omitted it defaults to TextDocumentSyncKind.None. + */ + Change TextDocumentSyncKind `json:"change,omitempty"` + /* + * If present will save notifications are sent to the server. If omitted the notification should not be + * sent. + */ + WillSave bool `json:"willSave,omitempty"` + /* + * If present will save wait until requests are sent to the server. If omitted the request should not be + * sent. + */ + WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` + /* + * If present save notifications are sent to the server. If omitted the notification should not be + * sent. + */ + Save SaveOptions `json:"save,omitempty"` +} + +// A text edit applicable to a text document. +type TextEdit struct { // line 4428 + /* + * The range of the text document to be manipulated. To insert + * text into a document create a range where start === end. + */ + Range Range `json:"range"` + /* + * The string to be inserted. For delete operations use an + * empty string. + */ + NewText string `json:"newText"` +} +type TokenFormat string // line 13762 +type TraceValues string // line 13409 +// Since 3.6.0 +type TypeDefinitionClientCapabilities struct { // line 11585 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `TypeDefinitionRegistrationOptions` return value + * for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + /* + * The client supports additional metadata in the form of definition links. + * + * Since 3.14.0 + */ + LinkSupport bool `json:"linkSupport,omitempty"` +} +type TypeDefinitionOptions struct { // line 6363 + WorkDoneProgressOptions +} +type TypeDefinitionParams struct { // line 2131 + TextDocumentPositionParams + WorkDoneProgressParams + PartialResultParams +} +type TypeDefinitionRegistrationOptions struct { // line 2151 + TextDocumentRegistrationOptions + TypeDefinitionOptions + StaticRegistrationOptions +} + +// @since 3.17.0 +type TypeHierarchyClientCapabilities struct { // line 12363 + /* + * Whether implementation supports dynamic registration. If this is set to `true` + * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` + * return value for the corresponding server capability as well. + */ + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +} + +// @since 3.17.0 +type TypeHierarchyItem struct { // line 3432 + // The name of this item. + Name string `json:"name"` + // The kind of this item. + Kind SymbolKind `json:"kind"` + // Tags for this item. + Tags []SymbolTag `json:"tags,omitempty"` + // More detail for this item, e.g. the signature of a function. + Detail string `json:"detail,omitempty"` + // The resource identifier of this item. + URI DocumentURI `json:"uri"` + /* + * The range enclosing this symbol not including leading/trailing whitespace + * but everything else, e.g. comments and code. + */ + Range Range `json:"range"` + /* + * The range that should be selected and revealed when this symbol is being + * picked, e.g. the name of a function. Must be contained by the + * [`range`](#TypeHierarchyItem.range). + */ + SelectionRange Range `json:"selectionRange"` + /* + * A data entry field that is preserved between a type hierarchy prepare and + * supertypes or subtypes requests. It could also be used to identify the + * type hierarchy in the server, helping improve the performance on + * resolving supertypes and subtypes. + */ + Data interface{} `json:"data,omitempty"` +} + +/* + * Type hierarchy options used during static registration. + * + * @since 3.17.0 + */ +type TypeHierarchyOptions struct { // line 6941 + WorkDoneProgressOptions +} + +/* + * The parameter of a `textDocument/prepareTypeHierarchy` request. + * + * @since 3.17.0 + */ +type TypeHierarchyPrepareParams struct { // line 3414 + TextDocumentPositionParams + WorkDoneProgressParams +} + +/* + * Type hierarchy options used during static or dynamic registration. + * + * @since 3.17.0 + */ +type TypeHierarchyRegistrationOptions struct { // line 3509 + TextDocumentRegistrationOptions + TypeHierarchyOptions + StaticRegistrationOptions +} + +/* + * The parameter of a `typeHierarchy/subtypes` request. + * + * @since 3.17.0 + */ +type TypeHierarchySubtypesParams struct { // line 3555 + Item TypeHierarchyItem `json:"item"` + WorkDoneProgressParams + PartialResultParams +} + +/* + * The parameter of a `typeHierarchy/supertypes` request. + * + * @since 3.17.0 + */ +type TypeHierarchySupertypesParams struct { // line 3531 + Item TypeHierarchyItem `json:"item"` + WorkDoneProgressParams + PartialResultParams +} + +// created for Tuple +type UIntCommaUInt struct { // line 10101 + Fld0 uint32 `json:"fld0"` + Fld1 uint32 `json:"fld1"` +} +type URI = string // (alias) line 0 +/* + * A diagnostic report indicating that the last returned + * report is still accurate. + * + * @since 3.17.0 + */ +type UnchangedDocumentDiagnosticReport struct { // line 7275 + /* + * A document diagnostic report indicating + * no changes to the last result. A server can + * only return `unchanged` if result ids are + * provided. + */ + Kind string `json:"kind"` + /* + * A result id which will be sent on the next + * diagnostic request for the same document. + */ + ResultID string `json:"resultId"` +} +type UniquenessLevel string // line 12997 +// General parameters to unregister a request or notification. +type Unregistration struct { // line 7633 + /* + * The id used to unregister the request or notification. Usually an id + * provided during the register request. + */ + ID string `json:"id"` + // The method to unregister for. + Method string `json:"method"` +} +type UnregistrationParams struct { // line 4075 + Unregisterations []Unregistration `json:"unregisterations"` +} + +/* + * A versioned notebook document identifier. + * + * @since 3.17.0 + */ +type VersionedNotebookDocumentIdentifier struct { // line 7448 + // The version number of this notebook document. + Version int32 `json:"version"` + // The notebook document's uri. + URI URI `json:"uri"` +} + +// A text document identifier to denote a specific version of a text document. +type VersionedTextDocumentIdentifier struct { // line 8465 + // The version number of this document. + Version int32 `json:"version"` + TextDocumentIdentifier +} +type WatchKind = uint32 // line 13505 +// The parameters sent in a will save text document notification. +type WillSaveTextDocumentParams struct { // line 4406 + // The document that will be saved. + TextDocument TextDocumentIdentifier `json:"textDocument"` + // The 'TextDocumentSaveReason'. + Reason TextDocumentSaveReason `json:"reason"` +} +type WindowClientCapabilities struct { // line 10655 + /* + * It indicates whether the client supports server initiated + * progress using the `window/workDoneProgress/create` request. + * + * The capability also controls Whether client supports handling + * of progress notifications. If set servers are allowed to report a + * `workDoneProgress` property in the request specific server + * capabilities. + * + * @since 3.15.0 + */ + WorkDoneProgress bool `json:"workDoneProgress,omitempty"` + /* + * Capabilities specific to the showMessage request. + * + * @since 3.16.0 + */ + ShowMessage *ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` + /* + * Capabilities specific to the showDocument request. + * + * @since 3.16.0 + */ + ShowDocument *ShowDocumentClientCapabilities `json:"showDocument,omitempty"` +} +type WorkDoneProgressBegin struct { // line 6059 + Kind string `json:"kind"` + /* + * Mandatory title of the progress operation. Used to briefly inform about + * the kind of operation being performed. + * + * Examples: \"Indexing\" or \"Linking dependencies\". + */ + Title string `json:"title"` + /* + * Controls if a cancel button should show to allow the user to cancel the + * long running operation. Clients that don't support cancellation are allowed + * to ignore the setting. + */ + Cancellable bool `json:"cancellable,omitempty"` + /* + * Optional, more detailed associated progress message. Contains + * complementary information to the `title`. + * + * Examples: \"3/25 files\", \"project/src/module2\", \"node_modules/some_dep\". + * If unset, the previous progress message (if any) is still valid. + */ + Message string `json:"message,omitempty"` + /* + * Optional progress percentage to display (value 100 is considered 100%). + * If not provided infinite progress is assumed and clients are allowed + * to ignore the `percentage` value in subsequent in report notifications. + * + * The value should be steadily rising. Clients are free to ignore values + * that are not following this rule. The value range is [0, 100]. + */ + Percentage uint32 `json:"percentage,omitempty"` +} +type WorkDoneProgressCancelParams struct { // line 2647 + // The token to be used to report progress. + Token ProgressToken `json:"token"` +} +type WorkDoneProgressCreateParams struct { // line 2634 + // The token to be used to report progress. + Token ProgressToken `json:"token"` +} +type WorkDoneProgressEnd struct { // line 6145 + Kind string `json:"kind"` + /* + * Optional, a final message indicating to for example indicate the outcome + * of the operation. + */ + Message string `json:"message,omitempty"` +} +type WorkDoneProgressOptions struct { // line 2377 + WorkDoneProgress bool `json:"workDoneProgress,omitempty"` +} + +// created for And +type WorkDoneProgressOptionsAndTextDocumentRegistrationOptions struct { // line 204 + WorkDoneProgressOptions + TextDocumentRegistrationOptions +} +type WorkDoneProgressParams struct { // line 6263 + // An optional token that a server can use to report work done progress. + WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` +} +type WorkDoneProgressReport struct { // line 6106 + Kind string `json:"kind"` + /* + * Controls enablement state of a cancel button. + * + * Clients that don't support cancellation or don't support controlling the button's + * enablement state are allowed to ignore the property. + */ + Cancellable bool `json:"cancellable,omitempty"` + /* + * Optional, more detailed associated progress message. Contains + * complementary information to the `title`. + * + * Examples: \"3/25 files\", \"project/src/module2\", \"node_modules/some_dep\". + * If unset, the previous progress message (if any) is still valid. + */ + Message string `json:"message,omitempty"` + /* + * Optional progress percentage to display (value 100 is considered 100%). + * If not provided infinite progress is assumed and clients are allowed + * to ignore the `percentage` value in subsequent in report notifications. + * + * The value should be steadily rising. Clients are free to ignore values + * that are not following this rule. The value range is [0, 100] + */ + Percentage uint32 `json:"percentage,omitempty"` +} + +// created for Literal +type Workspace6Gn struct { // line 8424 + /* + * The server supports workspace folder. + * + * @since 3.6.0 + */ + WorkspaceFolders WorkspaceFolders5Gn `json:"workspaceFolders"` + /* + * The server is interested in notifications/requests for operations on files. + * + * @since 3.16.0 + */ + FileOperations FileOperationOptions `json:"fileOperations"` +} + +// Workspace specific client capabilities. +type WorkspaceClientCapabilities struct { // line 10210 + /* + * The client supports applying batch edits + * to the workspace by supporting the request + * 'workspace/applyEdit' + */ + ApplyEdit bool `json:"applyEdit,omitempty"` + // Capabilities specific to `WorkspaceEdit`s. + WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` + // Capabilities specific to the `workspace/didChangeConfiguration` notification. + DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` + // Capabilities specific to the `workspace/didChangeWatchedFiles` notification. + DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` + // Capabilities specific to the `workspace/symbol` request. + Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` + // Capabilities specific to the `workspace/executeCommand` request. + ExecuteCommand *ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` + /* + * The client has support for workspace folders. + * + * @since 3.6.0 + */ + WorkspaceFolders bool `json:"workspaceFolders,omitempty"` + /* + * The client supports `workspace/configuration` requests. + * + * @since 3.6.0 + */ + Configuration bool `json:"configuration,omitempty"` + /* + * Capabilities specific to the semantic token requests scoped to the + * workspace. + * + * @since 3.16.0. + */ + SemanticTokens *SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` + /* + * Capabilities specific to the code lens requests scoped to the + * workspace. + * + * @since 3.16.0. + */ + CodeLens *CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` + /* + * The client has support for file notifications/requests for user operations on files. + * + * Since 3.16.0 + */ + FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` + /* + * Capabilities specific to the inline values requests scoped to the + * workspace. + * + * @since 3.17.0. + */ + InlineValue *InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` + /* + * Capabilities specific to the inlay hint requests scoped to the + * workspace. + * + * @since 3.17.0. + */ + InlayHint *InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` + /* + * Capabilities specific to the diagnostic requests scoped to the + * workspace. + * + * @since 3.17.0. + */ + Diagnostics *DiagnosticWorkspaceClientCapabilities `json:"diagnostics,omitempty"` +} + +/* + * Parameters of the workspace diagnostic request. + * + * @since 3.17.0 + */ +type WorkspaceDiagnosticParams struct { // line 3899 + // The additional identifier provided during registration. + Identifier string `json:"identifier,omitempty"` + /* + * The currently known diagnostic reports with their + * previous result ids. + */ + PreviousResultIds []PreviousResultID `json:"previousResultIds"` + WorkDoneProgressParams + PartialResultParams +} + +/* + * A workspace diagnostic report. + * + * @since 3.17.0 + */ +type WorkspaceDiagnosticReport struct { // line 3936 + Items []WorkspaceDocumentDiagnosticReport `json:"items"` +} + +/* + * A partial result for a workspace diagnostic report. + * + * @since 3.17.0 + */ +type WorkspaceDiagnosticReportPartialResult struct { // line 3953 + Items []WorkspaceDocumentDiagnosticReport `json:"items"` +} + +/* + * A workspace diagnostic document report. + * + * @since 3.17.0 + */ +type WorkspaceDocumentDiagnosticReport = Or_WorkspaceDocumentDiagnosticReport // (alias) line 14010 +/* + * A workspace edit represents changes to many resources managed in the workspace. The edit + * should either provide `changes` or `documentChanges`. If documentChanges are present + * they are preferred over `changes` if the client can handle versioned document edits. + * + * Since version 3.13.0 a workspace edit can contain resource operations as well. If resource + * operations are present clients need to execute the operations in the order in which they + * are provided. So a workspace edit for example can consist of the following two changes: + * (1) a create file a.txt and (2) a text document edit which insert text into file a.txt. + * + * An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will + * cause failure of the operation. How the client recovers from the failure is described by + * the client capability: `workspace.workspaceEdit.failureHandling` + */ +type WorkspaceEdit struct { // line 3215 + // Holds changes to existing resources. + Changes map[DocumentURI][]TextEdit `json:"changes,omitempty"` + /* + * Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes + * are either an array of `TextDocumentEdit`s to express changes to n different text documents + * where each text document edit addresses a specific version of a text document. Or it can contain + * above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. + * + * Whether a client supports versioned document edits is expressed via + * `workspace.workspaceEdit.documentChanges` client capability. + * + * If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then + * only plain `TextEdit`s using the `changes` property are supported. + */ + DocumentChanges []DocumentChanges `json:"documentChanges,omitempty"` + /* + * A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and + * delete file / folder operations. + * + * Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. + * + * @since 3.16.0 + */ + ChangeAnnotations map[ChangeAnnotationIdentifier]ChangeAnnotation `json:"changeAnnotations,omitempty"` +} +type WorkspaceEditClientCapabilities struct { // line 10794 + // The client supports versioned document changes in `WorkspaceEdit`s + DocumentChanges bool `json:"documentChanges,omitempty"` + /* + * The resource operations the client supports. Clients should at least + * support 'create', 'rename' and 'delete' files and folders. + * + * @since 3.13.0 + */ + ResourceOperations []ResourceOperationKind `json:"resourceOperations,omitempty"` + /* + * The failure handling strategy of a client if applying the workspace edit + * fails. + * + * @since 3.13.0 + */ + FailureHandling FailureHandlingKind `json:"failureHandling,omitempty"` + /* + * Whether the client normalizes line endings to the client specific + * setting. + * If set to `true` the client will normalize line ending characters + * in a workspace edit to the client-specified new line + * character. + * + * @since 3.16.0 + */ + NormalizesLineEndings bool `json:"normalizesLineEndings,omitempty"` + /* + * Whether the client in general supports change annotations on text edits, + * create file, rename file and delete file changes. + * + * @since 3.16.0 + */ + ChangeAnnotationSupport *PChangeAnnotationSupportPWorkspaceEdit `json:"changeAnnotationSupport,omitempty"` +} + +// A workspace folder inside a client. +type WorkspaceFolder struct { // line 2171 + // The associated URI for this workspace folder. + URI URI `json:"uri"` + /* + * The name of the workspace folder. Used to refer to this + * workspace folder in the user interface. + */ + Name string `json:"name"` +} +type WorkspaceFolders5Gn struct { // line 9959 + // The server has support for workspace folders + Supported bool `json:"supported,omitempty"` + /* + * Whether the server wants to receive workspace folder + * change notifications. + * + * If a string is provided the string is treated as an ID + * under which the notification is registered on the client + * side. The ID can be used to unregister for these events + * using the `client/unregisterCapability` request. + */ + ChangeNotifications string `json:"changeNotifications,omitempty"` +} + +// The workspace folder change event. +type WorkspaceFoldersChangeEvent struct { // line 6373 + // The array of added workspace folders + Added []WorkspaceFolder `json:"added"` + // The array of the removed workspace folders + Removed []WorkspaceFolder `json:"removed"` +} +type WorkspaceFoldersInitializeParams struct { // line 7802 + /* + * The workspace folders configured in the client when the server starts. + * + * This property is only available if the client supports workspace folders. + * It can be `null` if the client supports workspace folders but none are + * configured. + * + * @since 3.6.0 + */ + WorkspaceFolders []WorkspaceFolder `json:"workspaceFolders,omitempty"` +} +type WorkspaceFoldersServerCapabilities struct { // line 9959 + // The server has support for workspace folders + Supported bool `json:"supported,omitempty"` + /* + * Whether the server wants to receive workspace folder + * change notifications. + * + * If a string is provided the string is treated as an ID + * under which the notification is registered on the client + * side. The ID can be used to unregister for these events + * using the `client/unregisterCapability` request. + */ + ChangeNotifications string `json:"changeNotifications,omitempty"` +} + +/* + * A full document diagnostic report for a workspace diagnostic result. + * + * @since 3.17.0 + */ +type WorkspaceFullDocumentDiagnosticReport struct { // line 9542 + // The URI for which diagnostic information is reported. + URI DocumentURI `json:"uri"` + /* + * The version number for which the diagnostics are reported. + * If the document is not marked as open `null` can be provided. + */ + Version int32 `json:"version"` + FullDocumentDiagnosticReport +} + +/* + * A special workspace symbol that supports locations without a range. + * + * See also SymbolInformation. + * + * @since 3.17.0 + */ +type WorkspaceSymbol struct { // line 5534 + /* + * The location of the symbol. Whether a server is allowed to + * return a location without a range depends on the client + * capability `workspace.symbol.resolveSupport`. + * + * See SymbolInformation#location for more details. + */ + Location OrPLocation_workspace_symbol `json:"location"` + /* + * A data entry field that is preserved on a workspace symbol between a + * workspace symbol request and a workspace symbol resolve request. + */ + Data interface{} `json:"data,omitempty"` + BaseSymbolInformation +} + +// Client capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). +type WorkspaceSymbolClientCapabilities struct { // line 10901 + // Symbol request supports dynamic registration. + DynamicRegistration bool `json:"dynamicRegistration,omitempty"` + // Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. + SymbolKind *PSymbolKindPSymbol `json:"symbolKind,omitempty"` + /* + * The client supports tags on `SymbolInformation`. + * Clients supporting tags have to handle unknown tags gracefully. + * + * @since 3.16.0 + */ + TagSupport *PTagSupportPSymbol `json:"tagSupport,omitempty"` + /* + * The client support partial workspace symbols. The client will send the + * request `workspaceSymbol/resolve` to the server to resolve additional + * properties. + * + * @since 3.17.0 + */ + ResolveSupport *PResolveSupportPSymbol `json:"resolveSupport,omitempty"` +} + +// Server capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). +type WorkspaceSymbolOptions struct { // line 9125 + /* + * The server provides support to resolve additional + * information for a workspace symbol. + * + * @since 3.17.0 + */ + ResolveProvider bool `json:"resolveProvider,omitempty"` + WorkDoneProgressOptions +} + +// The parameters of a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). +type WorkspaceSymbolParams struct { // line 5510 + /* + * A query string to filter symbols by. Clients may send an empty + * string here to request all symbols. + */ + Query string `json:"query"` + WorkDoneProgressParams + PartialResultParams +} + +// Registration options for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). +type WorkspaceSymbolRegistrationOptions struct { // line 5583 + WorkspaceSymbolOptions +} + +/* + * An unchanged document diagnostic report for a workspace diagnostic result. + * + * @since 3.17.0 + */ +type WorkspaceUnchangedDocumentDiagnosticReport struct { // line 9580 + // The URI for which diagnostic information is reported. + URI DocumentURI `json:"uri"` + /* + * The version number for which the diagnostics are reported. + * If the document is not marked as open `null` can be provided. + */ + Version int32 `json:"version"` + UnchangedDocumentDiagnosticReport +} + +// The initialize parameters +type XInitializeParams struct { // line 7655 + /* + * The process Id of the parent process that started + * the server. + * + * Is `null` if the process has not been started by another process. + * If the parent process is not alive then the server should exit. + */ + ProcessID int32 `json:"processId"` + /* + * Information about the client + * + * @since 3.15.0 + */ + ClientInfo Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` + /* + * The locale the client is currently showing the user interface + * in. This must not necessarily be the locale of the operating + * system. + * + * Uses IETF language tags as the value's syntax + * (See https://en.wikipedia.org/wiki/IETF_language_tag) + * + * @since 3.16.0 + */ + Locale string `json:"locale,omitempty"` + /* + * The rootPath of the workspace. Is null + * if no folder is open. + * + * @deprecated in favour of rootUri. + */ + RootPath string `json:"rootPath,omitempty"` + /* + * The rootUri of the workspace. Is null if no + * folder is open. If both `rootPath` and `rootUri` are set + * `rootUri` wins. + * + * @deprecated in favour of workspaceFolders. + */ + RootURI DocumentURI `json:"rootUri"` + // The capabilities provided by the client (editor or tool) + Capabilities ClientCapabilities `json:"capabilities"` + // User provided initialization options. + InitializationOptions interface{} `json:"initializationOptions,omitempty"` + // The initial trace setting. If omitted trace is disabled ('off'). + Trace string `json:"trace,omitempty"` +} + +// The initialize parameters +type _InitializeParams struct { // line 7655 + /* + * The process Id of the parent process that started + * the server. + * + * Is `null` if the process has not been started by another process. + * If the parent process is not alive then the server should exit. + */ + ProcessID int32 `json:"processId"` + /* + * Information about the client + * + * @since 3.15.0 + */ + ClientInfo *Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` + /* + * The locale the client is currently showing the user interface + * in. This must not necessarily be the locale of the operating + * system. + * + * Uses IETF language tags as the value's syntax + * (See https://en.wikipedia.org/wiki/IETF_language_tag) + * + * @since 3.16.0 + */ + Locale string `json:"locale,omitempty"` + /* + * The rootPath of the workspace. Is null + * if no folder is open. + * + * @deprecated in favour of rootUri. + */ + RootPath string `json:"rootPath,omitempty"` + /* + * The rootUri of the workspace. Is null if no + * folder is open. If both `rootPath` and `rootUri` are set + * `rootUri` wins. + * + * @deprecated in favour of workspaceFolders. + */ + RootURI DocumentURI `json:"rootUri"` + // The capabilities provided by the client (editor or tool) + Capabilities ClientCapabilities `json:"capabilities"` + // User provided initialization options. + InitializationOptions interface{} `json:"initializationOptions,omitempty"` + // The initial trace setting. If omitted trace is disabled ('off'). + Trace string `json:"trace,omitempty"` +} + +const ( + // A set of predefined code action kinds + // Empty kind. + Empty CodeActionKind = "" // line 13359 + // Base kind for quickfix actions: 'quickfix' + QuickFix CodeActionKind = "quickfix" // line 13364 + // Base kind for refactoring actions: 'refactor' + Refactor CodeActionKind = "refactor" // line 13369 + /* + * Base kind for refactoring extraction actions: 'refactor.extract' + * + * Example extract actions: + * + * - Extract method + * - Extract function + * - Extract variable + * - Extract interface from class + * - ... + */ + RefactorExtract CodeActionKind = "refactor.extract" // line 13374 + /* + * Base kind for refactoring inline actions: 'refactor.inline' + * + * Example inline actions: + * + * - Inline function + * - Inline variable + * - Inline constant + * - ... + */ + RefactorInline CodeActionKind = "refactor.inline" // line 13379 + /* + * Base kind for refactoring rewrite actions: 'refactor.rewrite' + * + * Example rewrite actions: + * + * - Convert JavaScript function to class + * - Add or remove parameter + * - Encapsulate field + * - Make method static + * - Move method to base class + * - ... + */ + RefactorRewrite CodeActionKind = "refactor.rewrite" // line 13384 + /* + * Base kind for source actions: `source` + * + * Source code actions apply to the entire file. + */ + Source CodeActionKind = "source" // line 13389 + // Base kind for an organize imports source action: `source.organizeImports` + SourceOrganizeImports CodeActionKind = "source.organizeImports" // line 13394 + /* + * Base kind for auto-fix source actions: `source.fixAll`. + * + * Fix all actions automatically fix errors that have a clear fix that do not require user input. + * They should not suppress errors or perform unsafe fixes such as generating new types or classes. + * + * @since 3.15.0 + */ + SourceFixAll CodeActionKind = "source.fixAll" // line 13399 + /* + * The reason why code actions were requested. + * + * @since 3.17.0 + */ + // Code actions were explicitly requested by the user or by an extension. + CodeActionInvoked CodeActionTriggerKind = 1 // line 13639 + /* + * Code actions were requested automatically. + * + * This typically happens when current selection in a file changes, but can + * also be triggered when file content changes. + */ + CodeActionAutomatic CodeActionTriggerKind = 2 // line 13644 + // The kind of a completion entry. + TextCompletion CompletionItemKind = 1 // line 13167 + MethodCompletion CompletionItemKind = 2 // line 13171 + FunctionCompletion CompletionItemKind = 3 // line 13175 + ConstructorCompletion CompletionItemKind = 4 // line 13179 + FieldCompletion CompletionItemKind = 5 // line 13183 + VariableCompletion CompletionItemKind = 6 // line 13187 + ClassCompletion CompletionItemKind = 7 // line 13191 + InterfaceCompletion CompletionItemKind = 8 // line 13195 + ModuleCompletion CompletionItemKind = 9 // line 13199 + PropertyCompletion CompletionItemKind = 10 // line 13203 + UnitCompletion CompletionItemKind = 11 // line 13207 + ValueCompletion CompletionItemKind = 12 // line 13211 + EnumCompletion CompletionItemKind = 13 // line 13215 + KeywordCompletion CompletionItemKind = 14 // line 13219 + SnippetCompletion CompletionItemKind = 15 // line 13223 + ColorCompletion CompletionItemKind = 16 // line 13227 + FileCompletion CompletionItemKind = 17 // line 13231 + ReferenceCompletion CompletionItemKind = 18 // line 13235 + FolderCompletion CompletionItemKind = 19 // line 13239 + EnumMemberCompletion CompletionItemKind = 20 // line 13243 + ConstantCompletion CompletionItemKind = 21 // line 13247 + StructCompletion CompletionItemKind = 22 // line 13251 + EventCompletion CompletionItemKind = 23 // line 13255 + OperatorCompletion CompletionItemKind = 24 // line 13259 + TypeParameterCompletion CompletionItemKind = 25 // line 13263 + /* + * Completion item tags are extra annotations that tweak the rendering of a completion + * item. + * + * @since 3.15.0 + */ + // Render a completion as obsolete, usually using a strike-out. + ComplDeprecated CompletionItemTag = 1 // line 13277 + // How a completion was triggered + /* + * Completion was triggered by typing an identifier (24x7 code + * complete), manual invocation (e.g Ctrl+Space) or via API. + */ + Invoked CompletionTriggerKind = 1 // line 13588 + /* + * Completion was triggered by a trigger character specified by + * the `triggerCharacters` properties of the `CompletionRegistrationOptions`. + */ + TriggerCharacter CompletionTriggerKind = 2 // line 13593 + // Completion was re-triggered as current completion list is incomplete + TriggerForIncompleteCompletions CompletionTriggerKind = 3 // line 13598 + // The diagnostic's severity. + // Reports an error. + SeverityError DiagnosticSeverity = 1 // line 13537 + // Reports a warning. + SeverityWarning DiagnosticSeverity = 2 // line 13542 + // Reports an information. + SeverityInformation DiagnosticSeverity = 3 // line 13547 + // Reports a hint. + SeverityHint DiagnosticSeverity = 4 // line 13552 + /* + * The diagnostic tags. + * + * @since 3.15.0 + */ + /* + * Unused or unnecessary code. + * + * Clients are allowed to render diagnostics with this tag faded out instead of having + * an error squiggle. + */ + Unnecessary DiagnosticTag = 1 // line 13567 + /* + * Deprecated or obsolete code. + * + * Clients are allowed to rendered diagnostics with this tag strike through. + */ + Deprecated DiagnosticTag = 2 // line 13572 + /* + * The document diagnostic report kinds. + * + * @since 3.17.0 + */ + /* + * A diagnostic report with a full + * set of problems. + */ + DiagnosticFull DocumentDiagnosticReportKind = "full" // line 12755 + /* + * A report indicating that the last + * returned report is still accurate. + */ + DiagnosticUnchanged DocumentDiagnosticReportKind = "unchanged" // line 12760 + // A document highlight kind. + // A textual occurrence. + Text DocumentHighlightKind = 1 // line 13334 + // Read-access of a symbol, like reading a variable. + Read DocumentHighlightKind = 2 // line 13339 + // Write-access of a symbol, like writing to a variable. + Write DocumentHighlightKind = 3 // line 13344 + // Predefined error codes. + ParseError ErrorCodes = -32700 // line 12776 + InvalidRequest ErrorCodes = -32600 // line 12780 + MethodNotFound ErrorCodes = -32601 // line 12784 + InvalidParams ErrorCodes = -32602 // line 12788 + InternalError ErrorCodes = -32603 // line 12792 + /* + * Error code indicating that a server received a notification or + * request before the server has received the `initialize` request. + */ + ServerNotInitialized ErrorCodes = -32002 // line 12796 + UnknownErrorCode ErrorCodes = -32001 // line 12801 + /* + * Applying the workspace change is simply aborted if one of the changes provided + * fails. All operations executed before the failing operation stay executed. + */ + Abort FailureHandlingKind = "abort" // line 13726 + /* + * All operations are executed transactional. That means they either all + * succeed or no changes at all are applied to the workspace. + */ + Transactional FailureHandlingKind = "transactional" // line 13731 + /* + * If the workspace edit contains only textual file changes they are executed transactional. + * If resource changes (create, rename or delete file) are part of the change the failure + * handling strategy is abort. + */ + TextOnlyTransactional FailureHandlingKind = "textOnlyTransactional" // line 13736 + /* + * The client tries to undo the operations already executed. But there is no + * guarantee that this is succeeding. + */ + Undo FailureHandlingKind = "undo" // line 13741 + // The file event type + // The file got created. + Created FileChangeType = 1 // line 13487 + // The file got changed. + Changed FileChangeType = 2 // line 13492 + // The file got deleted. + Deleted FileChangeType = 3 // line 13497 + /* + * A pattern kind describing if a glob pattern matches a file a folder or + * both. + * + * @since 3.16.0 + */ + // The pattern matches a file only. + FilePattern FileOperationPatternKind = "file" // line 13660 + // The pattern matches a folder only. + FolderPattern FileOperationPatternKind = "folder" // line 13665 + // A set of predefined range kinds. + // Folding range for a comment + Comment FoldingRangeKind = "comment" // line 12848 + // Folding range for an import or include + Imports FoldingRangeKind = "imports" // line 12853 + // Folding range for a region (e.g. `#region`) + Region FoldingRangeKind = "region" // line 12858 + /* + * Inlay hint kinds. + * + * @since 3.17.0 + */ + // An inlay hint that for a type annotation. + Type InlayHintKind = 1 // line 13066 + // An inlay hint that is for a parameter. + Parameter InlayHintKind = 2 // line 13071 + /* + * Defines whether the insert text in a completion item should be interpreted as + * plain text or a snippet. + */ + // The primary text to be inserted is treated as a plain string. + PlainTextTextFormat InsertTextFormat = 1 // line 13293 + /* + * The primary text to be inserted is treated as a snippet. + * + * A snippet can define tab stops and placeholders with `$1`, `$2` + * and `${3:foo}`. `$0` defines the final tab stop, it defaults to + * the end of the snippet. Placeholders with equal identifiers are linked, + * that is typing in one will update others too. + * + * See also: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax + */ + SnippetTextFormat InsertTextFormat = 2 // line 13298 + /* + * How whitespace and indentation is handled during completion + * item insertion. + * + * @since 3.16.0 + */ + /* + * The insertion or replace strings is taken as it is. If the + * value is multi line the lines below the cursor will be + * inserted using the indentation defined in the string value. + * The client will not apply any kind of adjustments to the + * string. + */ + AsIs InsertTextMode = 1 // line 13313 + /* + * The editor adjusts leading whitespace of new lines so that + * they match the indentation up to the cursor of the line for + * which the item is accepted. + * + * Consider a line like this: <2tabs><3tabs>foo. Accepting a + * multi line completion item is indented using 2 tabs and all + * following lines inserted will be indented using 2 tabs as well. + */ + AdjustIndentation InsertTextMode = 2 // line 13318 + /* + * A request failed but it was syntactically correct, e.g the + * method name was known and the parameters were valid. The error + * message should contain human readable information about why + * the request failed. + * + * @since 3.17.0 + */ + RequestFailed LSPErrorCodes = -32803 // line 12816 + /* + * The server cancelled the request. This error code should + * only be used for requests that explicitly support being + * server cancellable. + * + * @since 3.17.0 + */ + ServerCancelled LSPErrorCodes = -32802 // line 12822 + /* + * The server detected that the content of a document got + * modified outside normal conditions. A server should + * NOT send this error code if it detects a content change + * in it unprocessed messages. The result even computed + * on an older state might still be useful for the client. + * + * If a client decides that a result is not of any use anymore + * the client should cancel the request. + */ + ContentModified LSPErrorCodes = -32801 // line 12828 + /* + * The client has canceled a request and a server as detected + * the cancel. + */ + RequestCancelled LSPErrorCodes = -32800 // line 12833 + /* + * Describes the content type that a client supports in various + * result literals like `Hover`, `ParameterInfo` or `CompletionItem`. + * + * Please note that `MarkupKinds` must not start with a `$`. This kinds + * are reserved for internal usage. + */ + // Plain text is supported as a content format + PlainText MarkupKind = "plaintext" // line 13440 + // Markdown is supported as a content format + Markdown MarkupKind = "markdown" // line 13445 + // The message type + // An error message. + Error MessageType = 1 // line 13087 + // A warning message. + Warning MessageType = 2 // line 13092 + // An information message. + Info MessageType = 3 // line 13097 + // A log message. + Log MessageType = 4 // line 13102 + /* + * The moniker kind. + * + * @since 3.16.0 + */ + // The moniker represent a symbol that is imported into a project + Import MonikerKind = "import" // line 13040 + // The moniker represents a symbol that is exported from a project + Export MonikerKind = "export" // line 13045 + /* + * The moniker represents a symbol that is local to a project (e.g. a local + * variable of a function, a class not visible outside the project, ...) + */ + Local MonikerKind = "local" // line 13050 + /* + * A notebook cell kind. + * + * @since 3.17.0 + */ + // A markup-cell is formatted source that is used for display. + Markup NotebookCellKind = 1 // line 13681 + // A code-cell is source code. + Code NotebookCellKind = 2 // line 13686 + /* + * A set of predefined position encoding kinds. + * + * @since 3.17.0 + */ + // Character offsets count UTF-8 code units. + UTF8 PositionEncodingKind = "utf-8" // line 13460 + /* + * Character offsets count UTF-16 code units. + * + * This is the default and must always be supported + * by servers + */ + UTF16 PositionEncodingKind = "utf-16" // line 13465 + /* + * Character offsets count UTF-32 code units. + * + * Implementation note: these are the same as Unicode code points, + * so this `PositionEncodingKind` may also be used for an + * encoding-agnostic representation of character offsets. + */ + UTF32 PositionEncodingKind = "utf-32" // line 13470 + // Supports creating new files and folders. + Create ResourceOperationKind = "create" // line 13702 + // Supports renaming existing files and folders. + Rename ResourceOperationKind = "rename" // line 13707 + // Supports deleting existing files and folders. + Delete ResourceOperationKind = "delete" // line 13712 + /* + * A set of predefined token modifiers. This set is not fixed + * an clients can specify additional token types via the + * corresponding client capabilities. + * + * @since 3.16.0 + */ + ModDeclaration SemanticTokenModifiers = "declaration" // line 12703 + ModDefinition SemanticTokenModifiers = "definition" // line 12707 + ModReadonly SemanticTokenModifiers = "readonly" // line 12711 + ModStatic SemanticTokenModifiers = "static" // line 12715 + ModDeprecated SemanticTokenModifiers = "deprecated" // line 12719 + ModAbstract SemanticTokenModifiers = "abstract" // line 12723 + ModAsync SemanticTokenModifiers = "async" // line 12727 + ModModification SemanticTokenModifiers = "modification" // line 12731 + ModDocumentation SemanticTokenModifiers = "documentation" // line 12735 + ModDefaultLibrary SemanticTokenModifiers = "defaultLibrary" // line 12739 + /* + * A set of predefined token types. This set is not fixed + * an clients can specify additional token types via the + * corresponding client capabilities. + * + * @since 3.16.0 + */ + NamespaceType SemanticTokenTypes = "namespace" // line 12596 + /* + * Represents a generic type. Acts as a fallback for types which can't be mapped to + * a specific type like class or enum. + */ + TypeType SemanticTokenTypes = "type" // line 12600 + ClassType SemanticTokenTypes = "class" // line 12605 + EnumType SemanticTokenTypes = "enum" // line 12609 + InterfaceType SemanticTokenTypes = "interface" // line 12613 + StructType SemanticTokenTypes = "struct" // line 12617 + TypeParameterType SemanticTokenTypes = "typeParameter" // line 12621 + ParameterType SemanticTokenTypes = "parameter" // line 12625 + VariableType SemanticTokenTypes = "variable" // line 12629 + PropertyType SemanticTokenTypes = "property" // line 12633 + EnumMemberType SemanticTokenTypes = "enumMember" // line 12637 + EventType SemanticTokenTypes = "event" // line 12641 + FunctionType SemanticTokenTypes = "function" // line 12645 + MethodType SemanticTokenTypes = "method" // line 12649 + MacroType SemanticTokenTypes = "macro" // line 12653 + KeywordType SemanticTokenTypes = "keyword" // line 12657 + ModifierType SemanticTokenTypes = "modifier" // line 12661 + CommentType SemanticTokenTypes = "comment" // line 12665 + StringType SemanticTokenTypes = "string" // line 12669 + NumberType SemanticTokenTypes = "number" // line 12673 + RegexpType SemanticTokenTypes = "regexp" // line 12677 + OperatorType SemanticTokenTypes = "operator" // line 12681 + // @since 3.17.0 + DecoratorType SemanticTokenTypes = "decorator" // line 12685 + /* + * How a signature help was triggered. + * + * @since 3.15.0 + */ + // Signature help was invoked manually by the user or by a command. + SigInvoked SignatureHelpTriggerKind = 1 // line 13613 + // Signature help was triggered by a trigger character. + SigTriggerCharacter SignatureHelpTriggerKind = 2 // line 13618 + // Signature help was triggered by the cursor moving or by the document content changing. + SigContentChange SignatureHelpTriggerKind = 3 // line 13623 + // A symbol kind. + File SymbolKind = 1 // line 12874 + Module SymbolKind = 2 // line 12878 + Namespace SymbolKind = 3 // line 12882 + Package SymbolKind = 4 // line 12886 + Class SymbolKind = 5 // line 12890 + Method SymbolKind = 6 // line 12894 + Property SymbolKind = 7 // line 12898 + Field SymbolKind = 8 // line 12902 + Constructor SymbolKind = 9 // line 12906 + Enum SymbolKind = 10 // line 12910 + Interface SymbolKind = 11 // line 12914 + Function SymbolKind = 12 // line 12918 + Variable SymbolKind = 13 // line 12922 + Constant SymbolKind = 14 // line 12926 + String SymbolKind = 15 // line 12930 + Number SymbolKind = 16 // line 12934 + Boolean SymbolKind = 17 // line 12938 + Array SymbolKind = 18 // line 12942 + Object SymbolKind = 19 // line 12946 + Key SymbolKind = 20 // line 12950 + Null SymbolKind = 21 // line 12954 + EnumMember SymbolKind = 22 // line 12958 + Struct SymbolKind = 23 // line 12962 + Event SymbolKind = 24 // line 12966 + Operator SymbolKind = 25 // line 12970 + TypeParameter SymbolKind = 26 // line 12974 + /* + * Symbol tags are extra annotations that tweak the rendering of a symbol. + * + * @since 3.16 + */ + // Render a symbol as obsolete, usually using a strike-out. + DeprecatedSymbol SymbolTag = 1 // line 12988 + // Represents reasons why a text document is saved. + /* + * Manually triggered, e.g. by the user pressing save, by starting debugging, + * or by an API call. + */ + Manual TextDocumentSaveReason = 1 // line 13142 + // Automatic after a delay. + AfterDelay TextDocumentSaveReason = 2 // line 13147 + // When the editor lost focus. + FocusOut TextDocumentSaveReason = 3 // line 13152 + /* + * Defines how the host (editor) should sync + * document changes to the language server. + */ + // Documents should not be synced at all. + None TextDocumentSyncKind = 0 // line 13117 + /* + * Documents are synced by always sending the full content + * of the document. + */ + Full TextDocumentSyncKind = 1 // line 13122 + /* + * Documents are synced by sending the full content on open. + * After that only incremental updates to the document are + * send. + */ + Incremental TextDocumentSyncKind = 2 // line 13127 + Relative TokenFormat = "relative" // line 13769 + // Turn tracing off. + Off TraceValues = "off" // line 13416 + // Trace messages only. + Messages TraceValues = "messages" // line 13421 + // Verbose message tracing. + Verbose TraceValues = "verbose" // line 13426 + /* + * Moniker uniqueness level to define scope of the moniker. + * + * @since 3.16.0 + */ + // The moniker is only unique inside a document + Document UniquenessLevel = "document" // line 13004 + // The moniker is unique inside a project for which a dump got created + Project UniquenessLevel = "project" // line 13009 + // The moniker is unique inside the group to which a project belongs + Group UniquenessLevel = "group" // line 13014 + // The moniker is unique inside the moniker scheme. + Scheme UniquenessLevel = "scheme" // line 13019 + // The moniker is globally unique + Global UniquenessLevel = "global" // line 13024 + // Interested in create events. + WatchCreate WatchKind = 1 // line 13512 + // Interested in change events + WatchChange WatchKind = 2 // line 13517 + // Interested in delete events + WatchDelete WatchKind = 4 // line 13522 +) diff --git a/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go similarity index 66% rename from internal/lsp/protocol/tsserver.go rename to gopls/internal/lsp/protocol/tsserver.go index a26e50cf4e5..a93ee80082b 100644 --- a/internal/lsp/protocol/tsserver.go +++ b/gopls/internal/lsp/protocol/tsserver.go @@ -1,273 +1,257 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// Copyright 2019-2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Code generated (see typescript/README.md) DO NOT EDIT. - package protocol -// Package protocol contains data types and code for LSP json rpcs -// generated automatically from vscode-languageserver-node -// commit: 696f9285bf849b73745682fdb1c1feac73eb8772 -// last fetched Fri Apr 01 2022 10:53:41 GMT-0400 (Eastern Daylight Time) +// Code generated from version 3.17.0 of protocol/metaModel.json. +// git hash 8de18faed635819dd2bc631d2c26ce4a18f7cf4a (as of Fri Sep 16 13:04:31 2022) +// Code generated; DO NOT EDIT. import ( "context" "encoding/json" - "fmt" "golang.org/x/tools/internal/jsonrpc2" ) type Server interface { - DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error - WorkDoneProgressCancel(context.Context, *WorkDoneProgressCancelParams) error - DidCreateFiles(context.Context, *CreateFilesParams) error - DidRenameFiles(context.Context, *RenameFilesParams) error - DidDeleteFiles(context.Context, *DeleteFilesParams) error - Initialized(context.Context, *InitializedParams) error - Exit(context.Context) error - DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error - DidOpen(context.Context, *DidOpenTextDocumentParams) error - DidChange(context.Context, *DidChangeTextDocumentParams) error - DidClose(context.Context, *DidCloseTextDocumentParams) error - DidSave(context.Context, *DidSaveTextDocumentParams) error - WillSave(context.Context, *WillSaveTextDocumentParams) error - DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error - DidOpenNotebookDocument(context.Context, *DidOpenNotebookDocumentParams) error - DidChangeNotebookDocument(context.Context, *DidChangeNotebookDocumentParams) error - DidSaveNotebookDocument(context.Context, *DidSaveNotebookDocumentParams) error - DidCloseNotebookDocument(context.Context, *DidCloseNotebookDocumentParams) error - SetTrace(context.Context, *SetTraceParams) error - LogTrace(context.Context, *LogTraceParams) error - Implementation(context.Context, *ImplementationParams) (Definition /*Definition | DefinitionLink[] | null*/, error) - TypeDefinition(context.Context, *TypeDefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) - DocumentColor(context.Context, *DocumentColorParams) ([]ColorInformation, error) - ColorPresentation(context.Context, *ColorPresentationParams) ([]ColorPresentation, error) - FoldingRange(context.Context, *FoldingRangeParams) ([]FoldingRange /*FoldingRange[] | null*/, error) - Declaration(context.Context, *DeclarationParams) (Declaration /*Declaration | DeclarationLink[] | null*/, error) - SelectionRange(context.Context, *SelectionRangeParams) ([]SelectionRange /*SelectionRange[] | null*/, error) - PrepareCallHierarchy(context.Context, *CallHierarchyPrepareParams) ([]CallHierarchyItem /*CallHierarchyItem[] | null*/, error) - IncomingCalls(context.Context, *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall /*CallHierarchyIncomingCall[] | null*/, error) - OutgoingCalls(context.Context, *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall /*CallHierarchyOutgoingCall[] | null*/, error) - SemanticTokensFull(context.Context, *SemanticTokensParams) (*SemanticTokens /*SemanticTokens | null*/, error) - SemanticTokensFullDelta(context.Context, *SemanticTokensDeltaParams) (interface{} /* SemanticTokens | SemanticTokensDelta | float64*/, error) - SemanticTokensRange(context.Context, *SemanticTokensRangeParams) (*SemanticTokens /*SemanticTokens | null*/, error) - SemanticTokensRefresh(context.Context) error - LinkedEditingRange(context.Context, *LinkedEditingRangeParams) (*LinkedEditingRanges /*LinkedEditingRanges | null*/, error) - WillCreateFiles(context.Context, *CreateFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) - WillRenameFiles(context.Context, *RenameFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) - WillDeleteFiles(context.Context, *DeleteFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) - Moniker(context.Context, *MonikerParams) ([]Moniker /*Moniker[] | null*/, error) - PrepareTypeHierarchy(context.Context, *TypeHierarchyPrepareParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) - Supertypes(context.Context, *TypeHierarchySupertypesParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) - Subtypes(context.Context, *TypeHierarchySubtypesParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) - InlineValue(context.Context, *InlineValueParams) ([]InlineValue /*InlineValue[] | null*/, error) - InlineValueRefresh(context.Context) error - InlayHint(context.Context, *InlayHintParams) ([]InlayHint /*InlayHint[] | null*/, error) - Resolve(context.Context, *InlayHint) (*InlayHint, error) - InlayHintRefresh(context.Context) error - Initialize(context.Context, *ParamInitialize) (*InitializeResult, error) - Shutdown(context.Context) error - WillSaveWaitUntil(context.Context, *WillSaveTextDocumentParams) ([]TextEdit /*TextEdit[] | null*/, error) - Completion(context.Context, *CompletionParams) (*CompletionList /*CompletionItem[] | CompletionList | null*/, error) - ResolveCompletionItem(context.Context, *CompletionItem) (*CompletionItem, error) - Hover(context.Context, *HoverParams) (*Hover /*Hover | null*/, error) - SignatureHelp(context.Context, *SignatureHelpParams) (*SignatureHelp /*SignatureHelp | null*/, error) - Definition(context.Context, *DefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) - References(context.Context, *ReferenceParams) ([]Location /*Location[] | null*/, error) - DocumentHighlight(context.Context, *DocumentHighlightParams) ([]DocumentHighlight /*DocumentHighlight[] | null*/, error) - DocumentSymbol(context.Context, *DocumentSymbolParams) ([]interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/, error) - CodeAction(context.Context, *CodeActionParams) ([]CodeAction /*(Command | CodeAction)[] | null*/, error) - ResolveCodeAction(context.Context, *CodeAction) (*CodeAction, error) - Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation /*SymbolInformation[] | WorkspaceSymbol[] | null*/, error) - ResolveWorkspaceSymbol(context.Context, *WorkspaceSymbol) (*WorkspaceSymbol, error) - CodeLens(context.Context, *CodeLensParams) ([]CodeLens /*CodeLens[] | null*/, error) - ResolveCodeLens(context.Context, *CodeLens) (*CodeLens, error) - CodeLensRefresh(context.Context) error - DocumentLink(context.Context, *DocumentLinkParams) ([]DocumentLink /*DocumentLink[] | null*/, error) - ResolveDocumentLink(context.Context, *DocumentLink) (*DocumentLink, error) - Formatting(context.Context, *DocumentFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) - RangeFormatting(context.Context, *DocumentRangeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) - OnTypeFormatting(context.Context, *DocumentOnTypeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) - Rename(context.Context, *RenameParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) - PrepareRename(context.Context, *PrepareRenameParams) (*PrepareRename2Gn /*Range | { range: Range; placeholder: string } | { defaultBehavior: boolean } | null*/, error) - ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{} /* LSPAny | void | float64*/, error) - Diagnostic(context.Context, *string) (*string, error) - DiagnosticWorkspace(context.Context, *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) - DiagnosticRefresh(context.Context) error + Progress(context.Context, *ProgressParams) error // $/progress + SetTrace(context.Context, *SetTraceParams) error // $/setTrace + IncomingCalls(context.Context, *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) // callHierarchy/incomingCalls + OutgoingCalls(context.Context, *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) // callHierarchy/outgoingCalls + ResolveCodeAction(context.Context, *CodeAction) (*CodeAction, error) // codeAction/resolve + ResolveCodeLens(context.Context, *CodeLens) (*CodeLens, error) // codeLens/resolve + ResolveCompletionItem(context.Context, *CompletionItem) (*CompletionItem, error) // completionItem/resolve + ResolveDocumentLink(context.Context, *DocumentLink) (*DocumentLink, error) // documentLink/resolve + Exit(context.Context) error // exit + Initialize(context.Context, *ParamInitialize) (*InitializeResult, error) // initialize + Initialized(context.Context, *InitializedParams) error // initialized + Resolve(context.Context, *InlayHint) (*InlayHint, error) // inlayHint/resolve + DidChangeNotebookDocument(context.Context, *DidChangeNotebookDocumentParams) error // notebookDocument/didChange + DidCloseNotebookDocument(context.Context, *DidCloseNotebookDocumentParams) error // notebookDocument/didClose + DidOpenNotebookDocument(context.Context, *DidOpenNotebookDocumentParams) error // notebookDocument/didOpen + DidSaveNotebookDocument(context.Context, *DidSaveNotebookDocumentParams) error // notebookDocument/didSave + Shutdown(context.Context) error // shutdown + CodeAction(context.Context, *CodeActionParams) ([]CodeAction, error) // textDocument/codeAction + CodeLens(context.Context, *CodeLensParams) ([]CodeLens, error) // textDocument/codeLens + ColorPresentation(context.Context, *ColorPresentationParams) ([]ColorPresentation, error) // textDocument/colorPresentation + Completion(context.Context, *CompletionParams) (*CompletionList, error) // textDocument/completion + Declaration(context.Context, *DeclarationParams) (*Or_textDocument_declaration, error) // textDocument/declaration + Definition(context.Context, *DefinitionParams) ([]Location, error) // textDocument/definition + Diagnostic(context.Context, *string) (*string, error) // textDocument/diagnostic + DidChange(context.Context, *DidChangeTextDocumentParams) error // textDocument/didChange + DidClose(context.Context, *DidCloseTextDocumentParams) error // textDocument/didClose + DidOpen(context.Context, *DidOpenTextDocumentParams) error // textDocument/didOpen + DidSave(context.Context, *DidSaveTextDocumentParams) error // textDocument/didSave + DocumentColor(context.Context, *DocumentColorParams) ([]ColorInformation, error) // textDocument/documentColor + DocumentHighlight(context.Context, *DocumentHighlightParams) ([]DocumentHighlight, error) // textDocument/documentHighlight + DocumentLink(context.Context, *DocumentLinkParams) ([]DocumentLink, error) // textDocument/documentLink + DocumentSymbol(context.Context, *DocumentSymbolParams) ([]interface{}, error) // textDocument/documentSymbol + FoldingRange(context.Context, *FoldingRangeParams) ([]FoldingRange, error) // textDocument/foldingRange + Formatting(context.Context, *DocumentFormattingParams) ([]TextEdit, error) // textDocument/formatting + Hover(context.Context, *HoverParams) (*Hover, error) // textDocument/hover + Implementation(context.Context, *ImplementationParams) ([]Location, error) // textDocument/implementation + InlayHint(context.Context, *InlayHintParams) ([]InlayHint, error) // textDocument/inlayHint + InlineValue(context.Context, *InlineValueParams) ([]InlineValue, error) // textDocument/inlineValue + LinkedEditingRange(context.Context, *LinkedEditingRangeParams) (*LinkedEditingRanges, error) // textDocument/linkedEditingRange + Moniker(context.Context, *MonikerParams) ([]Moniker, error) // textDocument/moniker + OnTypeFormatting(context.Context, *DocumentOnTypeFormattingParams) ([]TextEdit, error) // textDocument/onTypeFormatting + PrepareCallHierarchy(context.Context, *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) // textDocument/prepareCallHierarchy + PrepareRename(context.Context, *PrepareRenameParams) (*PrepareRename2Gn, error) // textDocument/prepareRename + PrepareTypeHierarchy(context.Context, *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) // textDocument/prepareTypeHierarchy + RangeFormatting(context.Context, *DocumentRangeFormattingParams) ([]TextEdit, error) // textDocument/rangeFormatting + References(context.Context, *ReferenceParams) ([]Location, error) // textDocument/references + Rename(context.Context, *RenameParams) (*WorkspaceEdit, error) // textDocument/rename + SelectionRange(context.Context, *SelectionRangeParams) ([]SelectionRange, error) // textDocument/selectionRange + SemanticTokensFull(context.Context, *SemanticTokensParams) (*SemanticTokens, error) // textDocument/semanticTokens/full + SemanticTokensFullDelta(context.Context, *SemanticTokensDeltaParams) (interface{}, error) // textDocument/semanticTokens/full/delta + SemanticTokensRange(context.Context, *SemanticTokensRangeParams) (*SemanticTokens, error) // textDocument/semanticTokens/range + SignatureHelp(context.Context, *SignatureHelpParams) (*SignatureHelp, error) // textDocument/signatureHelp + TypeDefinition(context.Context, *TypeDefinitionParams) ([]Location, error) // textDocument/typeDefinition + WillSave(context.Context, *WillSaveTextDocumentParams) error // textDocument/willSave + WillSaveWaitUntil(context.Context, *WillSaveTextDocumentParams) ([]TextEdit, error) // textDocument/willSaveWaitUntil + Subtypes(context.Context, *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/subtypes + Supertypes(context.Context, *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/supertypes + WorkDoneProgressCancel(context.Context, *WorkDoneProgressCancelParams) error // window/workDoneProgress/cancel + DiagnosticWorkspace(context.Context, *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) // workspace/diagnostic + DiagnosticRefresh(context.Context) error // workspace/diagnostic/refresh + DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error // workspace/didChangeConfiguration + DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error // workspace/didChangeWatchedFiles + DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error // workspace/didChangeWorkspaceFolders + DidCreateFiles(context.Context, *CreateFilesParams) error // workspace/didCreateFiles + DidDeleteFiles(context.Context, *DeleteFilesParams) error // workspace/didDeleteFiles + DidRenameFiles(context.Context, *RenameFilesParams) error // workspace/didRenameFiles + ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error) // workspace/executeCommand + InlayHintRefresh(context.Context) error // workspace/inlayHint/refresh + InlineValueRefresh(context.Context) error // workspace/inlineValue/refresh + SemanticTokensRefresh(context.Context) error // workspace/semanticTokens/refresh + Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error) // workspace/symbol + WillCreateFiles(context.Context, *CreateFilesParams) (*WorkspaceEdit, error) // workspace/willCreateFiles + WillDeleteFiles(context.Context, *DeleteFilesParams) (*WorkspaceEdit, error) // workspace/willDeleteFiles + WillRenameFiles(context.Context, *RenameFilesParams) (*WorkspaceEdit, error) // workspace/willRenameFiles + ResolveWorkspaceSymbol(context.Context, *WorkspaceSymbol) (*WorkspaceSymbol, error) // workspaceSymbol/resolve NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) } func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { switch r.Method() { - case "workspace/didChangeWorkspaceFolders": // notif - var params DidChangeWorkspaceFoldersParams + case "$/progress": + var params ProgressParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidChangeWorkspaceFolders(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "window/workDoneProgress/cancel": // notif - var params WorkDoneProgressCancelParams + err := server.Progress(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "$/setTrace": + var params SetTraceParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.WorkDoneProgressCancel(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/didCreateFiles": // notif - var params CreateFilesParams + err := server.SetTrace(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "callHierarchy/incomingCalls": + var params CallHierarchyIncomingCallsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidCreateFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/didRenameFiles": // notif - var params RenameFilesParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) + resp, err := server.IncomingCalls(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) } - err := server.DidRenameFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/didDeleteFiles": // notif - var params DeleteFilesParams + return true, reply(ctx, resp, nil) // 146 + case "callHierarchy/outgoingCalls": + var params CallHierarchyOutgoingCallsParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidDeleteFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "initialized": // notif - var params InitializedParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) + resp, err := server.OutgoingCalls(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) } - err := server.Initialized(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "exit": // notif - err := server.Exit(ctx) - return true, reply(ctx, nil, err) - case "workspace/didChangeConfiguration": // notif - var params DidChangeConfigurationParams + return true, reply(ctx, resp, nil) // 146 + case "codeAction/resolve": + var params CodeAction if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidChangeConfiguration(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/didOpen": // notif - var params DidOpenTextDocumentParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) + resp, err := server.ResolveCodeAction(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) } - err := server.DidOpen(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/didChange": // notif - var params DidChangeTextDocumentParams + return true, reply(ctx, resp, nil) // 146 + case "codeLens/resolve": + var params CodeLens if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidChange(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/didClose": // notif - var params DidCloseTextDocumentParams + resp, err := server.ResolveCodeLens(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "completionItem/resolve": + var params CompletionItem if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidClose(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/didSave": // notif - var params DidSaveTextDocumentParams + resp, err := server.ResolveCompletionItem(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "documentLink/resolve": + var params DocumentLink if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidSave(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/willSave": // notif - var params WillSaveTextDocumentParams + resp, err := server.ResolveDocumentLink(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "exit": + err := server.Exit(ctx) + return true, reply(ctx, nil, err) // 236 + case "initialize": + var params ParamInitialize if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.WillSave(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "workspace/didChangeWatchedFiles": // notif - var params DidChangeWatchedFilesParams + resp, err := server.Initialize(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "initialized": + var params InitializedParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidChangeWatchedFiles(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "notebookDocument/didOpen": // notif - var params DidOpenNotebookDocumentParams + err := server.Initialized(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "inlayHint/resolve": + var params InlayHint if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.DidOpenNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "notebookDocument/didChange": // notif + resp, err := server.Resolve(ctx, ¶ms) + if err != nil { + return true, reply(ctx, nil, err) + } + return true, reply(ctx, resp, nil) // 146 + case "notebookDocument/didChange": var params DidChangeNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidChangeNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "notebookDocument/didSave": // notif - var params DidSaveNotebookDocumentParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - } - err := server.DidSaveNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "notebookDocument/didClose": // notif + return true, reply(ctx, nil, err) // 231 + case "notebookDocument/didClose": var params DidCloseNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } err := server.DidCloseNotebookDocument(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "$/setTrace": // notif - var params SetTraceParams - if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - return true, sendParseError(ctx, reply, err) - } - err := server.SetTrace(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "$/logTrace": // notif - var params LogTraceParams + return true, reply(ctx, nil, err) // 231 + case "notebookDocument/didOpen": + var params DidOpenNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - err := server.LogTrace(ctx, ¶ms) - return true, reply(ctx, nil, err) - case "textDocument/implementation": // req - var params ImplementationParams + err := server.DidOpenNotebookDocument(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "notebookDocument/didSave": + var params DidSaveNotebookDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Implementation(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/typeDefinition": // req - var params TypeDefinitionParams + err := server.DidSaveNotebookDocument(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "shutdown": + err := server.Shutdown(ctx) + return true, reply(ctx, nil, err) // 176 + case "textDocument/codeAction": + var params CodeActionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.TypeDefinition(ctx, ¶ms) + resp, err := server.CodeAction(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/documentColor": // req - var params DocumentColorParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/codeLens": + var params CodeLensParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DocumentColor(ctx, ¶ms) + resp, err := server.CodeLens(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/colorPresentation": // req + return true, reply(ctx, resp, nil) // 146 + case "textDocument/colorPresentation": var params ColorPresentationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) @@ -276,18 +260,18 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/foldingRange": // req - var params FoldingRangeParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/completion": + var params CompletionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.FoldingRange(ctx, ¶ms) + resp, err := server.Completion(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/declaration": // req + return true, reply(ctx, resp, nil) // 146 + case "textDocument/declaration": var params DeclarationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) @@ -296,164 +280,146 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/selectionRange": // req - var params SelectionRangeParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/definition": + var params DefinitionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SelectionRange(ctx, ¶ms) + resp, err := server.Definition(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/prepareCallHierarchy": // req - var params CallHierarchyPrepareParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/diagnostic": + var params string if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.PrepareCallHierarchy(ctx, ¶ms) + resp, err := server.Diagnostic(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "callHierarchy/incomingCalls": // req - var params CallHierarchyIncomingCallsParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/didChange": + var params DidChangeTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.IncomingCalls(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "callHierarchy/outgoingCalls": // req - var params CallHierarchyOutgoingCallsParams + err := server.DidChange(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/didClose": + var params DidCloseTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.OutgoingCalls(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/semanticTokens/full": // req - var params SemanticTokensParams + err := server.DidClose(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/didOpen": + var params DidOpenTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SemanticTokensFull(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/semanticTokens/full/delta": // req - var params SemanticTokensDeltaParams + err := server.DidOpen(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/didSave": + var params DidSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SemanticTokensFullDelta(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/semanticTokens/range": // req - var params SemanticTokensRangeParams + err := server.DidSave(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/documentColor": + var params DocumentColorParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SemanticTokensRange(ctx, ¶ms) + resp, err := server.DocumentColor(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/semanticTokens/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.SemanticTokensRefresh(ctx) - return true, reply(ctx, nil, err) - case "textDocument/linkedEditingRange": // req - var params LinkedEditingRangeParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/documentHighlight": + var params DocumentHighlightParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.LinkedEditingRange(ctx, ¶ms) + resp, err := server.DocumentHighlight(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/willCreateFiles": // req - var params CreateFilesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/documentLink": + var params DocumentLinkParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.WillCreateFiles(ctx, ¶ms) + resp, err := server.DocumentLink(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/willRenameFiles": // req - var params RenameFilesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/documentSymbol": + var params DocumentSymbolParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.WillRenameFiles(ctx, ¶ms) + resp, err := server.DocumentSymbol(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/willDeleteFiles": // req - var params DeleteFilesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/foldingRange": + var params FoldingRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.WillDeleteFiles(ctx, ¶ms) + resp, err := server.FoldingRange(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/moniker": // req - var params MonikerParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/formatting": + var params DocumentFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Moniker(ctx, ¶ms) + resp, err := server.Formatting(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/prepareTypeHierarchy": // req - var params TypeHierarchyPrepareParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/hover": + var params HoverParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.PrepareTypeHierarchy(ctx, ¶ms) + resp, err := server.Hover(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "typeHierarchy/supertypes": // req - var params TypeHierarchySupertypesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/implementation": + var params ImplementationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Supertypes(ctx, ¶ms) + resp, err := server.Implementation(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "typeHierarchy/subtypes": // req - var params TypeHierarchySubtypesParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/inlayHint": + var params InlayHintParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Subtypes(ctx, ¶ms) + resp, err := server.InlayHint(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/inlineValue": // req + return true, reply(ctx, resp, nil) // 146 + case "textDocument/inlineValue": var params InlineValueParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) @@ -462,828 +428,755 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/inlineValue/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.InlineValueRefresh(ctx) - return true, reply(ctx, nil, err) - case "textDocument/inlayHint": // req - var params InlayHintParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/linkedEditingRange": + var params LinkedEditingRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.InlayHint(ctx, ¶ms) + resp, err := server.LinkedEditingRange(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "inlayHint/resolve": // req - var params InlayHint + return true, reply(ctx, resp, nil) // 146 + case "textDocument/moniker": + var params MonikerParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Resolve(ctx, ¶ms) + resp, err := server.Moniker(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/inlayHint/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.InlayHintRefresh(ctx) - return true, reply(ctx, nil, err) - case "initialize": // req - var params ParamInitialize + return true, reply(ctx, resp, nil) // 146 + case "textDocument/onTypeFormatting": + var params DocumentOnTypeFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { - if _, ok := err.(*json.UnmarshalTypeError); !ok { - return true, sendParseError(ctx, reply, err) - } + return true, sendParseError(ctx, reply, err) } - resp, err := server.Initialize(ctx, ¶ms) + resp, err := server.OnTypeFormatting(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "shutdown": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.Shutdown(ctx) - return true, reply(ctx, nil, err) - case "textDocument/willSaveWaitUntil": // req - var params WillSaveTextDocumentParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/prepareCallHierarchy": + var params CallHierarchyPrepareParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.WillSaveWaitUntil(ctx, ¶ms) + resp, err := server.PrepareCallHierarchy(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/completion": // req - var params CompletionParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/prepareRename": + var params PrepareRenameParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Completion(ctx, ¶ms) + resp, err := server.PrepareRename(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "completionItem/resolve": // req - var params CompletionItem + return true, reply(ctx, resp, nil) // 146 + case "textDocument/prepareTypeHierarchy": + var params TypeHierarchyPrepareParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveCompletionItem(ctx, ¶ms) + resp, err := server.PrepareTypeHierarchy(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/hover": // req - var params HoverParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/rangeFormatting": + var params DocumentRangeFormattingParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Hover(ctx, ¶ms) + resp, err := server.RangeFormatting(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/signatureHelp": // req - var params SignatureHelpParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/references": + var params ReferenceParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.SignatureHelp(ctx, ¶ms) + resp, err := server.References(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/definition": // req - var params DefinitionParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/rename": + var params RenameParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Definition(ctx, ¶ms) + resp, err := server.Rename(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/references": // req - var params ReferenceParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/selectionRange": + var params SelectionRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.References(ctx, ¶ms) + resp, err := server.SelectionRange(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/documentHighlight": // req - var params DocumentHighlightParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/semanticTokens/full": + var params SemanticTokensParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DocumentHighlight(ctx, ¶ms) + resp, err := server.SemanticTokensFull(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/documentSymbol": // req - var params DocumentSymbolParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/semanticTokens/full/delta": + var params SemanticTokensDeltaParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DocumentSymbol(ctx, ¶ms) + resp, err := server.SemanticTokensFullDelta(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/codeAction": // req - var params CodeActionParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/semanticTokens/range": + var params SemanticTokensRangeParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.CodeAction(ctx, ¶ms) + resp, err := server.SemanticTokensRange(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "codeAction/resolve": // req - var params CodeAction + return true, reply(ctx, resp, nil) // 146 + case "textDocument/signatureHelp": + var params SignatureHelpParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveCodeAction(ctx, ¶ms) + resp, err := server.SignatureHelp(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/symbol": // req - var params WorkspaceSymbolParams + return true, reply(ctx, resp, nil) // 146 + case "textDocument/typeDefinition": + var params TypeDefinitionParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Symbol(ctx, ¶ms) + resp, err := server.TypeDefinition(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspaceSymbol/resolve": // req - var params WorkspaceSymbol + return true, reply(ctx, resp, nil) // 146 + case "textDocument/willSave": + var params WillSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveWorkspaceSymbol(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) - } - return true, reply(ctx, resp, nil) - case "textDocument/codeLens": // req - var params CodeLensParams + err := server.WillSave(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "textDocument/willSaveWaitUntil": + var params WillSaveTextDocumentParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.CodeLens(ctx, ¶ms) + resp, err := server.WillSaveWaitUntil(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "codeLens/resolve": // req - var params CodeLens + return true, reply(ctx, resp, nil) // 146 + case "typeHierarchy/subtypes": + var params TypeHierarchySubtypesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveCodeLens(ctx, ¶ms) + resp, err := server.Subtypes(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/codeLens/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.CodeLensRefresh(ctx) - return true, reply(ctx, nil, err) - case "textDocument/documentLink": // req - var params DocumentLinkParams + return true, reply(ctx, resp, nil) // 146 + case "typeHierarchy/supertypes": + var params TypeHierarchySupertypesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DocumentLink(ctx, ¶ms) + resp, err := server.Supertypes(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "documentLink/resolve": // req - var params DocumentLink + return true, reply(ctx, resp, nil) // 146 + case "window/workDoneProgress/cancel": + var params WorkDoneProgressCancelParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ResolveDocumentLink(ctx, ¶ms) + err := server.WorkDoneProgressCancel(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/diagnostic": + var params WorkspaceDiagnosticParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + resp, err := server.DiagnosticWorkspace(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/formatting": // req - var params DocumentFormattingParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/diagnostic/refresh": + err := server.DiagnosticRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/didChangeConfiguration": + var params DidChangeConfigurationParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Formatting(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) + err := server.DidChangeConfiguration(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didChangeWatchedFiles": + var params DidChangeWatchedFilesParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) } - return true, reply(ctx, resp, nil) - case "textDocument/rangeFormatting": // req - var params DocumentRangeFormattingParams + err := server.DidChangeWatchedFiles(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didChangeWorkspaceFolders": + var params DidChangeWorkspaceFoldersParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.RangeFormatting(ctx, ¶ms) - if err != nil { - return true, reply(ctx, nil, err) + err := server.DidChangeWorkspaceFolders(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didCreateFiles": + var params CreateFilesParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) } - return true, reply(ctx, resp, nil) - case "textDocument/onTypeFormatting": // req - var params DocumentOnTypeFormattingParams + err := server.DidCreateFiles(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didDeleteFiles": + var params DeleteFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.OnTypeFormatting(ctx, ¶ms) + err := server.DidDeleteFiles(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/didRenameFiles": + var params RenameFilesParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + err := server.DidRenameFiles(ctx, ¶ms) + return true, reply(ctx, nil, err) // 231 + case "workspace/executeCommand": + var params ExecuteCommandParams + if err := json.Unmarshal(r.Params(), ¶ms); err != nil { + return true, sendParseError(ctx, reply, err) + } + resp, err := server.ExecuteCommand(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/rename": // req - var params RenameParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/inlayHint/refresh": + err := server.InlayHintRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/inlineValue/refresh": + err := server.InlineValueRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/semanticTokens/refresh": + err := server.SemanticTokensRefresh(ctx) + return true, reply(ctx, nil, err) // 170 + case "workspace/symbol": + var params WorkspaceSymbolParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Rename(ctx, ¶ms) + resp, err := server.Symbol(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/prepareRename": // req - var params PrepareRenameParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/willCreateFiles": + var params CreateFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.PrepareRename(ctx, ¶ms) + resp, err := server.WillCreateFiles(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/executeCommand": // req - var params ExecuteCommandParams + return true, reply(ctx, resp, nil) // 146 + case "workspace/willDeleteFiles": + var params DeleteFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.ExecuteCommand(ctx, ¶ms) + resp, err := server.WillDeleteFiles(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "textDocument/diagnostic": // req - var params string + return true, reply(ctx, resp, nil) // 146 + case "workspace/willRenameFiles": + var params RenameFilesParams if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.Diagnostic(ctx, ¶ms) + resp, err := server.WillRenameFiles(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/diagnostic": // req - var params WorkspaceDiagnosticParams + return true, reply(ctx, resp, nil) // 146 + case "workspaceSymbol/resolve": + var params WorkspaceSymbol if err := json.Unmarshal(r.Params(), ¶ms); err != nil { return true, sendParseError(ctx, reply, err) } - resp, err := server.DiagnosticWorkspace(ctx, ¶ms) + resp, err := server.ResolveWorkspaceSymbol(ctx, ¶ms) if err != nil { return true, reply(ctx, nil, err) } - return true, reply(ctx, resp, nil) - case "workspace/diagnostic/refresh": // req - if len(r.Params()) > 0 { - return true, reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) - } - err := server.DiagnosticRefresh(ctx) - return true, reply(ctx, nil, err) - + return true, reply(ctx, resp, nil) // 146 default: return false, nil } } -func (s *serverDispatcher) DidChangeWorkspaceFolders(ctx context.Context, params *DidChangeWorkspaceFoldersParams) error { - return s.sender.Notify(ctx, "workspace/didChangeWorkspaceFolders", params) -} - -func (s *serverDispatcher) WorkDoneProgressCancel(ctx context.Context, params *WorkDoneProgressCancelParams) error { - return s.sender.Notify(ctx, "window/workDoneProgress/cancel", params) -} - -func (s *serverDispatcher) DidCreateFiles(ctx context.Context, params *CreateFilesParams) error { - return s.sender.Notify(ctx, "workspace/didCreateFiles", params) -} - -func (s *serverDispatcher) DidRenameFiles(ctx context.Context, params *RenameFilesParams) error { - return s.sender.Notify(ctx, "workspace/didRenameFiles", params) -} - -func (s *serverDispatcher) DidDeleteFiles(ctx context.Context, params *DeleteFilesParams) error { - return s.sender.Notify(ctx, "workspace/didDeleteFiles", params) -} - -func (s *serverDispatcher) Initialized(ctx context.Context, params *InitializedParams) error { - return s.sender.Notify(ctx, "initialized", params) -} - -func (s *serverDispatcher) Exit(ctx context.Context) error { - return s.sender.Notify(ctx, "exit", nil) -} - -func (s *serverDispatcher) DidChangeConfiguration(ctx context.Context, params *DidChangeConfigurationParams) error { - return s.sender.Notify(ctx, "workspace/didChangeConfiguration", params) -} - -func (s *serverDispatcher) DidOpen(ctx context.Context, params *DidOpenTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/didOpen", params) -} - -func (s *serverDispatcher) DidChange(ctx context.Context, params *DidChangeTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/didChange", params) -} - -func (s *serverDispatcher) DidClose(ctx context.Context, params *DidCloseTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/didClose", params) -} - -func (s *serverDispatcher) DidSave(ctx context.Context, params *DidSaveTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/didSave", params) -} - -func (s *serverDispatcher) WillSave(ctx context.Context, params *WillSaveTextDocumentParams) error { - return s.sender.Notify(ctx, "textDocument/willSave", params) -} - -func (s *serverDispatcher) DidChangeWatchedFiles(ctx context.Context, params *DidChangeWatchedFilesParams) error { - return s.sender.Notify(ctx, "workspace/didChangeWatchedFiles", params) -} - -func (s *serverDispatcher) DidOpenNotebookDocument(ctx context.Context, params *DidOpenNotebookDocumentParams) error { - return s.sender.Notify(ctx, "notebookDocument/didOpen", params) -} - -func (s *serverDispatcher) DidChangeNotebookDocument(ctx context.Context, params *DidChangeNotebookDocumentParams) error { - return s.sender.Notify(ctx, "notebookDocument/didChange", params) -} - -func (s *serverDispatcher) DidSaveNotebookDocument(ctx context.Context, params *DidSaveNotebookDocumentParams) error { - return s.sender.Notify(ctx, "notebookDocument/didSave", params) -} - -func (s *serverDispatcher) DidCloseNotebookDocument(ctx context.Context, params *DidCloseNotebookDocumentParams) error { - return s.sender.Notify(ctx, "notebookDocument/didClose", params) -} - +func (s *serverDispatcher) Progress(ctx context.Context, params *ProgressParams) error { + return s.sender.Notify(ctx, "$/progress", params) +} // 244 func (s *serverDispatcher) SetTrace(ctx context.Context, params *SetTraceParams) error { return s.sender.Notify(ctx, "$/setTrace", params) -} - -func (s *serverDispatcher) LogTrace(ctx context.Context, params *LogTraceParams) error { - return s.sender.Notify(ctx, "$/logTrace", params) -} -func (s *serverDispatcher) Implementation(ctx context.Context, params *ImplementationParams) (Definition /*Definition | DefinitionLink[] | null*/, error) { - var result Definition /*Definition | DefinitionLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/implementation", params, &result); err != nil { +} // 244 +func (s *serverDispatcher) IncomingCalls(ctx context.Context, params *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) { + var result []CallHierarchyIncomingCall + if err := s.sender.Call(ctx, "callHierarchy/incomingCalls", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) TypeDefinition(ctx context.Context, params *TypeDefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) { - var result Definition /*Definition | DefinitionLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/typeDefinition", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) OutgoingCalls(ctx context.Context, params *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) { + var result []CallHierarchyOutgoingCall + if err := s.sender.Call(ctx, "callHierarchy/outgoingCalls", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DocumentColor(ctx context.Context, params *DocumentColorParams) ([]ColorInformation, error) { - var result []ColorInformation - if err := s.sender.Call(ctx, "textDocument/documentColor", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveCodeAction(ctx context.Context, params *CodeAction) (*CodeAction, error) { + var result *CodeAction + if err := s.sender.Call(ctx, "codeAction/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ColorPresentation(ctx context.Context, params *ColorPresentationParams) ([]ColorPresentation, error) { - var result []ColorPresentation - if err := s.sender.Call(ctx, "textDocument/colorPresentation", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveCodeLens(ctx context.Context, params *CodeLens) (*CodeLens, error) { + var result *CodeLens + if err := s.sender.Call(ctx, "codeLens/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) FoldingRange(ctx context.Context, params *FoldingRangeParams) ([]FoldingRange /*FoldingRange[] | null*/, error) { - var result []FoldingRange /*FoldingRange[] | null*/ - if err := s.sender.Call(ctx, "textDocument/foldingRange", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveCompletionItem(ctx context.Context, params *CompletionItem) (*CompletionItem, error) { + var result *CompletionItem + if err := s.sender.Call(ctx, "completionItem/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Declaration(ctx context.Context, params *DeclarationParams) (Declaration /*Declaration | DeclarationLink[] | null*/, error) { - var result Declaration /*Declaration | DeclarationLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/declaration", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveDocumentLink(ctx context.Context, params *DocumentLink) (*DocumentLink, error) { + var result *DocumentLink + if err := s.sender.Call(ctx, "documentLink/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SelectionRange(ctx context.Context, params *SelectionRangeParams) ([]SelectionRange /*SelectionRange[] | null*/, error) { - var result []SelectionRange /*SelectionRange[] | null*/ - if err := s.sender.Call(ctx, "textDocument/selectionRange", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Exit(ctx context.Context) error { + return s.sender.Notify(ctx, "exit", nil) +} // 249 +func (s *serverDispatcher) Initialize(ctx context.Context, params *ParamInitialize) (*InitializeResult, error) { + var result *InitializeResult + if err := s.sender.Call(ctx, "initialize", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) PrepareCallHierarchy(ctx context.Context, params *CallHierarchyPrepareParams) ([]CallHierarchyItem /*CallHierarchyItem[] | null*/, error) { - var result []CallHierarchyItem /*CallHierarchyItem[] | null*/ - if err := s.sender.Call(ctx, "textDocument/prepareCallHierarchy", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Initialized(ctx context.Context, params *InitializedParams) error { + return s.sender.Notify(ctx, "initialized", params) +} // 244 +func (s *serverDispatcher) Resolve(ctx context.Context, params *InlayHint) (*InlayHint, error) { + var result *InlayHint + if err := s.sender.Call(ctx, "inlayHint/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) IncomingCalls(ctx context.Context, params *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall /*CallHierarchyIncomingCall[] | null*/, error) { - var result []CallHierarchyIncomingCall /*CallHierarchyIncomingCall[] | null*/ - if err := s.sender.Call(ctx, "callHierarchy/incomingCalls", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DidChangeNotebookDocument(ctx context.Context, params *DidChangeNotebookDocumentParams) error { + return s.sender.Notify(ctx, "notebookDocument/didChange", params) +} // 244 +func (s *serverDispatcher) DidCloseNotebookDocument(ctx context.Context, params *DidCloseNotebookDocumentParams) error { + return s.sender.Notify(ctx, "notebookDocument/didClose", params) +} // 244 +func (s *serverDispatcher) DidOpenNotebookDocument(ctx context.Context, params *DidOpenNotebookDocumentParams) error { + return s.sender.Notify(ctx, "notebookDocument/didOpen", params) +} // 244 +func (s *serverDispatcher) DidSaveNotebookDocument(ctx context.Context, params *DidSaveNotebookDocumentParams) error { + return s.sender.Notify(ctx, "notebookDocument/didSave", params) +} // 244 +func (s *serverDispatcher) Shutdown(ctx context.Context) error { + return s.sender.Call(ctx, "shutdown", nil, nil) +} // 209 +func (s *serverDispatcher) CodeAction(ctx context.Context, params *CodeActionParams) ([]CodeAction, error) { + var result []CodeAction + if err := s.sender.Call(ctx, "textDocument/codeAction", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) OutgoingCalls(ctx context.Context, params *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall /*CallHierarchyOutgoingCall[] | null*/, error) { - var result []CallHierarchyOutgoingCall /*CallHierarchyOutgoingCall[] | null*/ - if err := s.sender.Call(ctx, "callHierarchy/outgoingCalls", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) CodeLens(ctx context.Context, params *CodeLensParams) ([]CodeLens, error) { + var result []CodeLens + if err := s.sender.Call(ctx, "textDocument/codeLens", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SemanticTokensFull(ctx context.Context, params *SemanticTokensParams) (*SemanticTokens /*SemanticTokens | null*/, error) { - var result *SemanticTokens /*SemanticTokens | null*/ - if err := s.sender.Call(ctx, "textDocument/semanticTokens/full", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ColorPresentation(ctx context.Context, params *ColorPresentationParams) ([]ColorPresentation, error) { + var result []ColorPresentation + if err := s.sender.Call(ctx, "textDocument/colorPresentation", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SemanticTokensFullDelta(ctx context.Context, params *SemanticTokensDeltaParams) (interface{} /* SemanticTokens | SemanticTokensDelta | float64*/, error) { - var result interface{} /* SemanticTokens | SemanticTokensDelta | float64*/ - if err := s.sender.Call(ctx, "textDocument/semanticTokens/full/delta", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Completion(ctx context.Context, params *CompletionParams) (*CompletionList, error) { + var result *CompletionList + if err := s.sender.Call(ctx, "textDocument/completion", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SemanticTokensRange(ctx context.Context, params *SemanticTokensRangeParams) (*SemanticTokens /*SemanticTokens | null*/, error) { - var result *SemanticTokens /*SemanticTokens | null*/ - if err := s.sender.Call(ctx, "textDocument/semanticTokens/range", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Declaration(ctx context.Context, params *DeclarationParams) (*Or_textDocument_declaration, error) { + var result *Or_textDocument_declaration + if err := s.sender.Call(ctx, "textDocument/declaration", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SemanticTokensRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) -} - -func (s *serverDispatcher) LinkedEditingRange(ctx context.Context, params *LinkedEditingRangeParams) (*LinkedEditingRanges /*LinkedEditingRanges | null*/, error) { - var result *LinkedEditingRanges /*LinkedEditingRanges | null*/ - if err := s.sender.Call(ctx, "textDocument/linkedEditingRange", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Definition(ctx context.Context, params *DefinitionParams) ([]Location, error) { + var result []Location + if err := s.sender.Call(ctx, "textDocument/definition", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) WillCreateFiles(ctx context.Context, params *CreateFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) { - var result *WorkspaceEdit /*WorkspaceEdit | null*/ - if err := s.sender.Call(ctx, "workspace/willCreateFiles", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Diagnostic(ctx context.Context, params *string) (*string, error) { + var result *string + if err := s.sender.Call(ctx, "textDocument/diagnostic", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) WillRenameFiles(ctx context.Context, params *RenameFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) { - var result *WorkspaceEdit /*WorkspaceEdit | null*/ - if err := s.sender.Call(ctx, "workspace/willRenameFiles", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DidChange(ctx context.Context, params *DidChangeTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/didChange", params) +} // 244 +func (s *serverDispatcher) DidClose(ctx context.Context, params *DidCloseTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/didClose", params) +} // 244 +func (s *serverDispatcher) DidOpen(ctx context.Context, params *DidOpenTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/didOpen", params) +} // 244 +func (s *serverDispatcher) DidSave(ctx context.Context, params *DidSaveTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/didSave", params) +} // 244 +func (s *serverDispatcher) DocumentColor(ctx context.Context, params *DocumentColorParams) ([]ColorInformation, error) { + var result []ColorInformation + if err := s.sender.Call(ctx, "textDocument/documentColor", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) WillDeleteFiles(ctx context.Context, params *DeleteFilesParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) { - var result *WorkspaceEdit /*WorkspaceEdit | null*/ - if err := s.sender.Call(ctx, "workspace/willDeleteFiles", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DocumentHighlight(ctx context.Context, params *DocumentHighlightParams) ([]DocumentHighlight, error) { + var result []DocumentHighlight + if err := s.sender.Call(ctx, "textDocument/documentHighlight", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Moniker(ctx context.Context, params *MonikerParams) ([]Moniker /*Moniker[] | null*/, error) { - var result []Moniker /*Moniker[] | null*/ - if err := s.sender.Call(ctx, "textDocument/moniker", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DocumentLink(ctx context.Context, params *DocumentLinkParams) ([]DocumentLink, error) { + var result []DocumentLink + if err := s.sender.Call(ctx, "textDocument/documentLink", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) PrepareTypeHierarchy(ctx context.Context, params *TypeHierarchyPrepareParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) { - var result []TypeHierarchyItem /*TypeHierarchyItem[] | null*/ - if err := s.sender.Call(ctx, "textDocument/prepareTypeHierarchy", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]interface{}, error) { + var result []interface{} + if err := s.sender.Call(ctx, "textDocument/documentSymbol", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Supertypes(ctx context.Context, params *TypeHierarchySupertypesParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) { - var result []TypeHierarchyItem /*TypeHierarchyItem[] | null*/ - if err := s.sender.Call(ctx, "typeHierarchy/supertypes", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) FoldingRange(ctx context.Context, params *FoldingRangeParams) ([]FoldingRange, error) { + var result []FoldingRange + if err := s.sender.Call(ctx, "textDocument/foldingRange", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Subtypes(ctx context.Context, params *TypeHierarchySubtypesParams) ([]TypeHierarchyItem /*TypeHierarchyItem[] | null*/, error) { - var result []TypeHierarchyItem /*TypeHierarchyItem[] | null*/ - if err := s.sender.Call(ctx, "typeHierarchy/subtypes", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Formatting(ctx context.Context, params *DocumentFormattingParams) ([]TextEdit, error) { + var result []TextEdit + if err := s.sender.Call(ctx, "textDocument/formatting", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) InlineValue(ctx context.Context, params *InlineValueParams) ([]InlineValue /*InlineValue[] | null*/, error) { - var result []InlineValue /*InlineValue[] | null*/ - if err := s.sender.Call(ctx, "textDocument/inlineValue", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Hover(ctx context.Context, params *HoverParams) (*Hover, error) { + var result *Hover + if err := s.sender.Call(ctx, "textDocument/hover", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) InlineValueRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) -} - -func (s *serverDispatcher) InlayHint(ctx context.Context, params *InlayHintParams) ([]InlayHint /*InlayHint[] | null*/, error) { - var result []InlayHint /*InlayHint[] | null*/ - if err := s.sender.Call(ctx, "textDocument/inlayHint", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Implementation(ctx context.Context, params *ImplementationParams) ([]Location, error) { + var result []Location + if err := s.sender.Call(ctx, "textDocument/implementation", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Resolve(ctx context.Context, params *InlayHint) (*InlayHint, error) { - var result *InlayHint - if err := s.sender.Call(ctx, "inlayHint/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) InlayHint(ctx context.Context, params *InlayHintParams) ([]InlayHint, error) { + var result []InlayHint + if err := s.sender.Call(ctx, "textDocument/inlayHint", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) InlayHintRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) -} - -func (s *serverDispatcher) Initialize(ctx context.Context, params *ParamInitialize) (*InitializeResult, error) { - var result *InitializeResult - if err := s.sender.Call(ctx, "initialize", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) InlineValue(ctx context.Context, params *InlineValueParams) ([]InlineValue, error) { + var result []InlineValue + if err := s.sender.Call(ctx, "textDocument/inlineValue", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Shutdown(ctx context.Context) error { - return s.sender.Call(ctx, "shutdown", nil, nil) -} - -func (s *serverDispatcher) WillSaveWaitUntil(ctx context.Context, params *WillSaveTextDocumentParams) ([]TextEdit /*TextEdit[] | null*/, error) { - var result []TextEdit /*TextEdit[] | null*/ - if err := s.sender.Call(ctx, "textDocument/willSaveWaitUntil", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) LinkedEditingRange(ctx context.Context, params *LinkedEditingRangeParams) (*LinkedEditingRanges, error) { + var result *LinkedEditingRanges + if err := s.sender.Call(ctx, "textDocument/linkedEditingRange", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Completion(ctx context.Context, params *CompletionParams) (*CompletionList /*CompletionItem[] | CompletionList | null*/, error) { - var result *CompletionList /*CompletionItem[] | CompletionList | null*/ - if err := s.sender.Call(ctx, "textDocument/completion", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Moniker(ctx context.Context, params *MonikerParams) ([]Moniker, error) { + var result []Moniker + if err := s.sender.Call(ctx, "textDocument/moniker", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveCompletionItem(ctx context.Context, params *CompletionItem) (*CompletionItem, error) { - var result *CompletionItem - if err := s.sender.Call(ctx, "completionItem/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) OnTypeFormatting(ctx context.Context, params *DocumentOnTypeFormattingParams) ([]TextEdit, error) { + var result []TextEdit + if err := s.sender.Call(ctx, "textDocument/onTypeFormatting", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Hover(ctx context.Context, params *HoverParams) (*Hover /*Hover | null*/, error) { - var result *Hover /*Hover | null*/ - if err := s.sender.Call(ctx, "textDocument/hover", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) PrepareCallHierarchy(ctx context.Context, params *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) { + var result []CallHierarchyItem + if err := s.sender.Call(ctx, "textDocument/prepareCallHierarchy", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) SignatureHelp(ctx context.Context, params *SignatureHelpParams) (*SignatureHelp /*SignatureHelp | null*/, error) { - var result *SignatureHelp /*SignatureHelp | null*/ - if err := s.sender.Call(ctx, "textDocument/signatureHelp", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (*PrepareRename2Gn, error) { + var result *PrepareRename2Gn + if err := s.sender.Call(ctx, "textDocument/prepareRename", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Definition(ctx context.Context, params *DefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) { - var result Definition /*Definition | DefinitionLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/definition", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) PrepareTypeHierarchy(ctx context.Context, params *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) { + var result []TypeHierarchyItem + if err := s.sender.Call(ctx, "textDocument/prepareTypeHierarchy", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) References(ctx context.Context, params *ReferenceParams) ([]Location /*Location[] | null*/, error) { - var result []Location /*Location[] | null*/ - if err := s.sender.Call(ctx, "textDocument/references", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) RangeFormatting(ctx context.Context, params *DocumentRangeFormattingParams) ([]TextEdit, error) { + var result []TextEdit + if err := s.sender.Call(ctx, "textDocument/rangeFormatting", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DocumentHighlight(ctx context.Context, params *DocumentHighlightParams) ([]DocumentHighlight /*DocumentHighlight[] | null*/, error) { - var result []DocumentHighlight /*DocumentHighlight[] | null*/ - if err := s.sender.Call(ctx, "textDocument/documentHighlight", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) References(ctx context.Context, params *ReferenceParams) ([]Location, error) { + var result []Location + if err := s.sender.Call(ctx, "textDocument/references", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/, error) { - var result []interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/ - if err := s.sender.Call(ctx, "textDocument/documentSymbol", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Rename(ctx context.Context, params *RenameParams) (*WorkspaceEdit, error) { + var result *WorkspaceEdit + if err := s.sender.Call(ctx, "textDocument/rename", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) CodeAction(ctx context.Context, params *CodeActionParams) ([]CodeAction /*(Command | CodeAction)[] | null*/, error) { - var result []CodeAction /*(Command | CodeAction)[] | null*/ - if err := s.sender.Call(ctx, "textDocument/codeAction", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SelectionRange(ctx context.Context, params *SelectionRangeParams) ([]SelectionRange, error) { + var result []SelectionRange + if err := s.sender.Call(ctx, "textDocument/selectionRange", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveCodeAction(ctx context.Context, params *CodeAction) (*CodeAction, error) { - var result *CodeAction - if err := s.sender.Call(ctx, "codeAction/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SemanticTokensFull(ctx context.Context, params *SemanticTokensParams) (*SemanticTokens, error) { + var result *SemanticTokens + if err := s.sender.Call(ctx, "textDocument/semanticTokens/full", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation /*SymbolInformation[] | WorkspaceSymbol[] | null*/, error) { - var result []SymbolInformation /*SymbolInformation[] | WorkspaceSymbol[] | null*/ - if err := s.sender.Call(ctx, "workspace/symbol", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SemanticTokensFullDelta(ctx context.Context, params *SemanticTokensDeltaParams) (interface{}, error) { + var result interface{} + if err := s.sender.Call(ctx, "textDocument/semanticTokens/full/delta", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveWorkspaceSymbol(ctx context.Context, params *WorkspaceSymbol) (*WorkspaceSymbol, error) { - var result *WorkspaceSymbol - if err := s.sender.Call(ctx, "workspaceSymbol/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SemanticTokensRange(ctx context.Context, params *SemanticTokensRangeParams) (*SemanticTokens, error) { + var result *SemanticTokens + if err := s.sender.Call(ctx, "textDocument/semanticTokens/range", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) CodeLens(ctx context.Context, params *CodeLensParams) ([]CodeLens /*CodeLens[] | null*/, error) { - var result []CodeLens /*CodeLens[] | null*/ - if err := s.sender.Call(ctx, "textDocument/codeLens", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) SignatureHelp(ctx context.Context, params *SignatureHelpParams) (*SignatureHelp, error) { + var result *SignatureHelp + if err := s.sender.Call(ctx, "textDocument/signatureHelp", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveCodeLens(ctx context.Context, params *CodeLens) (*CodeLens, error) { - var result *CodeLens - if err := s.sender.Call(ctx, "codeLens/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) TypeDefinition(ctx context.Context, params *TypeDefinitionParams) ([]Location, error) { + var result []Location + if err := s.sender.Call(ctx, "textDocument/typeDefinition", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) CodeLensRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/codeLens/refresh", nil, nil) -} - -func (s *serverDispatcher) DocumentLink(ctx context.Context, params *DocumentLinkParams) ([]DocumentLink /*DocumentLink[] | null*/, error) { - var result []DocumentLink /*DocumentLink[] | null*/ - if err := s.sender.Call(ctx, "textDocument/documentLink", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WillSave(ctx context.Context, params *WillSaveTextDocumentParams) error { + return s.sender.Notify(ctx, "textDocument/willSave", params) +} // 244 +func (s *serverDispatcher) WillSaveWaitUntil(ctx context.Context, params *WillSaveTextDocumentParams) ([]TextEdit, error) { + var result []TextEdit + if err := s.sender.Call(ctx, "textDocument/willSaveWaitUntil", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ResolveDocumentLink(ctx context.Context, params *DocumentLink) (*DocumentLink, error) { - var result *DocumentLink - if err := s.sender.Call(ctx, "documentLink/resolve", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Subtypes(ctx context.Context, params *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) { + var result []TypeHierarchyItem + if err := s.sender.Call(ctx, "typeHierarchy/subtypes", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Formatting(ctx context.Context, params *DocumentFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) { - var result []TextEdit /*TextEdit[] | null*/ - if err := s.sender.Call(ctx, "textDocument/formatting", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) Supertypes(ctx context.Context, params *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) { + var result []TypeHierarchyItem + if err := s.sender.Call(ctx, "typeHierarchy/supertypes", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) RangeFormatting(ctx context.Context, params *DocumentRangeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) { - var result []TextEdit /*TextEdit[] | null*/ - if err := s.sender.Call(ctx, "textDocument/rangeFormatting", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WorkDoneProgressCancel(ctx context.Context, params *WorkDoneProgressCancelParams) error { + return s.sender.Notify(ctx, "window/workDoneProgress/cancel", params) +} // 244 +func (s *serverDispatcher) DiagnosticWorkspace(ctx context.Context, params *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) { + var result *WorkspaceDiagnosticReport + if err := s.sender.Call(ctx, "workspace/diagnostic", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) OnTypeFormatting(ctx context.Context, params *DocumentOnTypeFormattingParams) ([]TextEdit /*TextEdit[] | null*/, error) { - var result []TextEdit /*TextEdit[] | null*/ - if err := s.sender.Call(ctx, "textDocument/onTypeFormatting", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) DiagnosticRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) +} // 209 +func (s *serverDispatcher) DidChangeConfiguration(ctx context.Context, params *DidChangeConfigurationParams) error { + return s.sender.Notify(ctx, "workspace/didChangeConfiguration", params) +} // 244 +func (s *serverDispatcher) DidChangeWatchedFiles(ctx context.Context, params *DidChangeWatchedFilesParams) error { + return s.sender.Notify(ctx, "workspace/didChangeWatchedFiles", params) +} // 244 +func (s *serverDispatcher) DidChangeWorkspaceFolders(ctx context.Context, params *DidChangeWorkspaceFoldersParams) error { + return s.sender.Notify(ctx, "workspace/didChangeWorkspaceFolders", params) +} // 244 +func (s *serverDispatcher) DidCreateFiles(ctx context.Context, params *CreateFilesParams) error { + return s.sender.Notify(ctx, "workspace/didCreateFiles", params) +} // 244 +func (s *serverDispatcher) DidDeleteFiles(ctx context.Context, params *DeleteFilesParams) error { + return s.sender.Notify(ctx, "workspace/didDeleteFiles", params) +} // 244 +func (s *serverDispatcher) DidRenameFiles(ctx context.Context, params *RenameFilesParams) error { + return s.sender.Notify(ctx, "workspace/didRenameFiles", params) +} // 244 +func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCommandParams) (interface{}, error) { + var result interface{} + if err := s.sender.Call(ctx, "workspace/executeCommand", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Rename(ctx context.Context, params *RenameParams) (*WorkspaceEdit /*WorkspaceEdit | null*/, error) { - var result *WorkspaceEdit /*WorkspaceEdit | null*/ - if err := s.sender.Call(ctx, "textDocument/rename", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) InlayHintRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) +} // 209 +func (s *serverDispatcher) InlineValueRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) +} // 209 +func (s *serverDispatcher) SemanticTokensRefresh(ctx context.Context) error { + return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) +} // 209 +func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) { + var result []SymbolInformation + if err := s.sender.Call(ctx, "workspace/symbol", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (*PrepareRename2Gn /*Range | { range: Range; placeholder: string } | { defaultBehavior: boolean } | null*/, error) { - var result *PrepareRename2Gn /*Range | { range: Range; placeholder: string } | { defaultBehavior: boolean } | null*/ - if err := s.sender.Call(ctx, "textDocument/prepareRename", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WillCreateFiles(ctx context.Context, params *CreateFilesParams) (*WorkspaceEdit, error) { + var result *WorkspaceEdit + if err := s.sender.Call(ctx, "workspace/willCreateFiles", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCommandParams) (interface{} /* LSPAny | void | float64*/, error) { - var result interface{} /* LSPAny | void | float64*/ - if err := s.sender.Call(ctx, "workspace/executeCommand", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WillDeleteFiles(ctx context.Context, params *DeleteFilesParams) (*WorkspaceEdit, error) { + var result *WorkspaceEdit + if err := s.sender.Call(ctx, "workspace/willDeleteFiles", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) Diagnostic(ctx context.Context, params *string) (*string, error) { - var result *string - if err := s.sender.Call(ctx, "textDocument/diagnostic", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) WillRenameFiles(ctx context.Context, params *RenameFilesParams) (*WorkspaceEdit, error) { + var result *WorkspaceEdit + if err := s.sender.Call(ctx, "workspace/willRenameFiles", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DiagnosticWorkspace(ctx context.Context, params *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) { - var result *WorkspaceDiagnosticReport - if err := s.sender.Call(ctx, "workspace/diagnostic", params, &result); err != nil { +} // 169 +func (s *serverDispatcher) ResolveWorkspaceSymbol(ctx context.Context, params *WorkspaceSymbol) (*WorkspaceSymbol, error) { + var result *WorkspaceSymbol + if err := s.sender.Call(ctx, "workspaceSymbol/resolve", params, &result); err != nil { return nil, err } return result, nil -} - -func (s *serverDispatcher) DiagnosticRefresh(ctx context.Context) error { - return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) -} - +} // 169 func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { var result interface{} if err := s.sender.Call(ctx, method, params, &result); err != nil { diff --git a/internal/lsp/protocol/typescript/README.md b/gopls/internal/lsp/protocol/typescript/README.md similarity index 100% rename from internal/lsp/protocol/typescript/README.md rename to gopls/internal/lsp/protocol/typescript/README.md diff --git a/internal/lsp/protocol/typescript/code.ts b/gopls/internal/lsp/protocol/typescript/code.ts similarity index 100% rename from internal/lsp/protocol/typescript/code.ts rename to gopls/internal/lsp/protocol/typescript/code.ts diff --git a/internal/lsp/protocol/typescript/tsconfig.json b/gopls/internal/lsp/protocol/typescript/tsconfig.json similarity index 100% rename from internal/lsp/protocol/typescript/tsconfig.json rename to gopls/internal/lsp/protocol/typescript/tsconfig.json diff --git a/internal/lsp/protocol/typescript/util.ts b/gopls/internal/lsp/protocol/typescript/util.ts similarity index 100% rename from internal/lsp/protocol/typescript/util.ts rename to gopls/internal/lsp/protocol/typescript/util.ts diff --git a/internal/lsp/references.go b/gopls/internal/lsp/references.go similarity index 87% rename from internal/lsp/references.go rename to gopls/internal/lsp/references.go index f96e5532cb5..6f4e3ee2060 100644 --- a/internal/lsp/references.go +++ b/gopls/internal/lsp/references.go @@ -7,9 +7,9 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" ) func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) { diff --git a/internal/lsp/regtest/doc.go b/gopls/internal/lsp/regtest/doc.go similarity index 95% rename from internal/lsp/regtest/doc.go rename to gopls/internal/lsp/regtest/doc.go index e97276965b1..39eddd8dcff 100644 --- a/internal/lsp/regtest/doc.go +++ b/gopls/internal/lsp/regtest/doc.go @@ -19,7 +19,7 @@ // - the Env type provides a collection of resources to use in writing tests // (for example a temporary working directory and fake text editor) // - user interactions with these resources are scripted using test wrappers -// around the API provided by the golang.org/x/tools/internal/lsp/fake +// around the API provided by the golang.org/x/tools/gopls/internal/lsp/fake // package. // // Regressions are expressed in terms of Expectations, which at a high level diff --git a/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go similarity index 51% rename from internal/lsp/regtest/env.go rename to gopls/internal/lsp/regtest/env.go index f095c38f285..2374dab19ef 100644 --- a/internal/lsp/regtest/env.go +++ b/gopls/internal/lsp/regtest/env.go @@ -11,27 +11,39 @@ import ( "sync" "testing" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" ) -// Env holds an initialized fake Editor, Workspace, and Server, which may be -// used for writing tests. It also provides adapter methods that call t.Fatal -// on any error, so that tests for the happy path may be written without -// checking errors. +// Env holds the building blocks of an editor testing environment, providing +// wrapper methods that hide the boilerplate of plumbing contexts and checking +// errors. type Env struct { - T testing.TB + T testing.TB // TODO(rfindley): rename to TB Ctx context.Context // Most tests should not need to access the scratch area, editor, server, or // connection, but they are available if needed. Sandbox *fake.Sandbox - Editor *fake.Editor Server servertest.Connector - // mu guards the fields below, for the purpose of checking conditions on - // every change to diagnostics. + // Editor is owned by the Env, and shut down + Editor *fake.Editor + + Awaiter *Awaiter +} + +// An Awaiter keeps track of relevant LSP state, so that it may be asserted +// upon with Expectations. +// +// Wire it into a fake.Editor using Awaiter.Hooks(). +// +// TODO(rfindley): consider simply merging Awaiter with the fake.Editor. It +// probably is not worth its own abstraction. +type Awaiter struct { + workdir *fake.Workdir + mu sync.Mutex // For simplicity, each waiter gets a unique ID. nextWaiterID int @@ -39,6 +51,32 @@ type Env struct { waiters map[int]*condition } +func NewAwaiter(workdir *fake.Workdir) *Awaiter { + return &Awaiter{ + workdir: workdir, + state: State{ + diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), + outstandingWork: make(map[protocol.ProgressToken]*workProgress), + startedWork: make(map[string]uint64), + completedWork: make(map[string]uint64), + }, + waiters: make(map[int]*condition), + } +} + +func (a *Awaiter) Hooks() fake.ClientHooks { + return fake.ClientHooks{ + OnDiagnostics: a.onDiagnostics, + OnLogMessage: a.onLogMessage, + OnWorkDoneProgressCreate: a.onWorkDoneProgressCreate, + OnProgress: a.onProgress, + OnShowMessage: a.onShowMessage, + OnShowMessageRequest: a.onShowMessageRequest, + OnRegistration: a.onRegistration, + OnUnregistration: a.onUnregistration, + } +} + // State encapsulates the server state TODO: explain more type State struct { // diagnostics are a map of relative path->diagnostics params @@ -47,8 +85,9 @@ type State struct { showMessage []*protocol.ShowMessageParams showMessageRequest []*protocol.ShowMessageRequestParams - registrations []*protocol.RegistrationParams - unregistrations []*protocol.UnregistrationParams + registrations []*protocol.RegistrationParams + registeredCapabilities map[string]protocol.Registration + unregistrations []*protocol.UnregistrationParams // outstandingWork is a map of token->work summary. All tokens are assumed to // be string, though the spec allows for numeric tokens as well. When work @@ -80,7 +119,7 @@ func (s State) String() string { for name, params := range s.diagnostics { fmt.Fprintf(&b, "\t%s (version %d):\n", name, int(params.Version)) for _, d := range params.Diagnostics { - fmt.Fprintf(&b, "\t\t(%d, %d): %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Message) + fmt.Fprintf(&b, "\t\t(%d, %d) [%s]: %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Source, d.Message) } } b.WriteString("\n") @@ -107,94 +146,55 @@ type condition struct { verdict chan Verdict } -// NewEnv creates a new test environment using the given scratch environment -// and gopls server. -func NewEnv(ctx context.Context, tb testing.TB, sandbox *fake.Sandbox, ts servertest.Connector, editorConfig fake.EditorConfig, withHooks bool) *Env { - tb.Helper() - conn := ts.Connect(ctx) - env := &Env{ - T: tb, - Ctx: ctx, - Sandbox: sandbox, - Server: ts, - state: State{ - diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), - outstandingWork: make(map[protocol.ProgressToken]*workProgress), - startedWork: make(map[string]uint64), - completedWork: make(map[string]uint64), - }, - waiters: make(map[int]*condition), - } - var hooks fake.ClientHooks - if withHooks { - hooks = fake.ClientHooks{ - OnDiagnostics: env.onDiagnostics, - OnLogMessage: env.onLogMessage, - OnWorkDoneProgressCreate: env.onWorkDoneProgressCreate, - OnProgress: env.onProgress, - OnShowMessage: env.onShowMessage, - OnShowMessageRequest: env.onShowMessageRequest, - OnRegistration: env.onRegistration, - OnUnregistration: env.onUnregistration, - } - } - editor, err := fake.NewEditor(sandbox, editorConfig).Connect(ctx, conn, hooks) - if err != nil { - tb.Fatal(err) - } - env.Editor = editor - return env -} - -func (e *Env) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { + a.mu.Lock() + defer a.mu.Unlock() - pth := e.Sandbox.Workdir.URIToPath(d.URI) - e.state.diagnostics[pth] = d - e.checkConditionsLocked() + pth := a.workdir.URIToPath(d.URI) + a.state.diagnostics[pth] = d + a.checkConditionsLocked() return nil } -func (e *Env) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.showMessage = append(e.state.showMessage, m) - e.checkConditionsLocked() + a.state.showMessage = append(a.state.showMessage, m) + a.checkConditionsLocked() return nil } -func (e *Env) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.showMessageRequest = append(e.state.showMessageRequest, m) - e.checkConditionsLocked() + a.state.showMessageRequest = append(a.state.showMessageRequest, m) + a.checkConditionsLocked() return nil } -func (e *Env) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.logs = append(e.state.logs, m) - e.checkConditionsLocked() + a.state.logs = append(a.state.logs, m) + a.checkConditionsLocked() return nil } -func (e *Env) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.outstandingWork[m.Token] = &workProgress{} + a.state.outstandingWork[m.Token] = &workProgress{} return nil } -func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error { - e.mu.Lock() - defer e.mu.Unlock() - work, ok := e.state.outstandingWork[m.Token] +func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) error { + a.mu.Lock() + defer a.mu.Unlock() + work, ok := a.state.outstandingWork[m.Token] if !ok { panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m)) } @@ -202,7 +202,7 @@ func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error { switch kind := v["kind"]; kind { case "begin": work.title = v["title"].(string) - e.state.startedWork[work.title] = e.state.startedWork[work.title] + 1 + a.state.startedWork[work.title] = a.state.startedWork[work.title] + 1 if msg, ok := v["message"]; ok { work.msg = msg.(string) } @@ -214,36 +214,42 @@ func (e *Env) onProgress(_ context.Context, m *protocol.ProgressParams) error { work.msg = msg.(string) } case "end": - title := e.state.outstandingWork[m.Token].title - e.state.completedWork[title] = e.state.completedWork[title] + 1 - delete(e.state.outstandingWork, m.Token) + title := a.state.outstandingWork[m.Token].title + a.state.completedWork[title] = a.state.completedWork[title] + 1 + delete(a.state.outstandingWork, m.Token) } - e.checkConditionsLocked() + a.checkConditionsLocked() return nil } -func (e *Env) onRegistration(_ context.Context, m *protocol.RegistrationParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onRegistration(_ context.Context, m *protocol.RegistrationParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.registrations = append(e.state.registrations, m) - e.checkConditionsLocked() + a.state.registrations = append(a.state.registrations, m) + if a.state.registeredCapabilities == nil { + a.state.registeredCapabilities = make(map[string]protocol.Registration) + } + for _, reg := range m.Registrations { + a.state.registeredCapabilities[reg.Method] = reg + } + a.checkConditionsLocked() return nil } -func (e *Env) onUnregistration(_ context.Context, m *protocol.UnregistrationParams) error { - e.mu.Lock() - defer e.mu.Unlock() +func (a *Awaiter) onUnregistration(_ context.Context, m *protocol.UnregistrationParams) error { + a.mu.Lock() + defer a.mu.Unlock() - e.state.unregistrations = append(e.state.unregistrations, m) - e.checkConditionsLocked() + a.state.unregistrations = append(a.state.unregistrations, m) + a.checkConditionsLocked() return nil } -func (e *Env) checkConditionsLocked() { - for id, condition := range e.waiters { - if v, _ := checkExpectations(e.state, condition.expectations); v != Unmet { - delete(e.waiters, id) +func (a *Awaiter) checkConditionsLocked() { + for id, condition := range a.waiters { + if v, _ := checkExpectations(a.state, condition.expectations); v != Unmet { + delete(a.waiters, id) condition.verdict <- v } } @@ -266,53 +272,63 @@ func checkExpectations(s State, expectations []Expectation) (Verdict, string) { // DiagnosticsFor returns the current diagnostics for the file. It is useful // after waiting on AnyDiagnosticAtCurrentVersion, when the desired diagnostic // is not simply described by DiagnosticAt. -func (e *Env) DiagnosticsFor(name string) *protocol.PublishDiagnosticsParams { - e.mu.Lock() - defer e.mu.Unlock() - return e.state.diagnostics[name] +// +// TODO(rfindley): this method is inherently racy. Replace usages of this +// method with the atomic OnceMet(..., ReadDiagnostics) pattern. +func (a *Awaiter) DiagnosticsFor(name string) *protocol.PublishDiagnosticsParams { + a.mu.Lock() + defer a.mu.Unlock() + return a.state.diagnostics[name] } -// Await waits for all expectations to simultaneously be met. It should only be -// called from the main test goroutine. func (e *Env) Await(expectations ...Expectation) { e.T.Helper() - e.mu.Lock() + if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { + e.T.Fatal(err) + } +} + +// Await waits for all expectations to simultaneously be met. It should only be +// called from the main test goroutine. +func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error { + a.mu.Lock() // Before adding the waiter, we check if the condition is currently met or // failed to avoid a race where the condition was realized before Await was // called. - switch verdict, summary := checkExpectations(e.state, expectations); verdict { + switch verdict, summary := checkExpectations(a.state, expectations); verdict { case Met: - e.mu.Unlock() - return + a.mu.Unlock() + return nil case Unmeetable: - failure := fmt.Sprintf("unmeetable expectations:\n%s\nstate:\n%v", summary, e.state) - e.mu.Unlock() - e.T.Fatal(failure) + err := fmt.Errorf("unmeetable expectations:\n%s\nstate:\n%v", summary, a.state) + a.mu.Unlock() + return err } cond := &condition{ expectations: expectations, verdict: make(chan Verdict), } - e.waiters[e.nextWaiterID] = cond - e.nextWaiterID++ - e.mu.Unlock() + a.waiters[a.nextWaiterID] = cond + a.nextWaiterID++ + a.mu.Unlock() var err error select { - case <-e.Ctx.Done(): - err = e.Ctx.Err() + case <-ctx.Done(): + err = ctx.Err() case v := <-cond.verdict: if v != Met { err = fmt.Errorf("condition has final verdict %v", v) } } - e.mu.Lock() - defer e.mu.Unlock() - _, summary := checkExpectations(e.state, expectations) + a.mu.Lock() + defer a.mu.Unlock() + _, summary := checkExpectations(a.state, expectations) // Debugging an unmet expectation can be tricky, so we put some effort into // nicely formatting the failure. if err != nil { - e.T.Fatalf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, e.state) + return fmt.Errorf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, a.state) } + return nil } diff --git a/internal/lsp/regtest/env_test.go b/gopls/internal/lsp/regtest/env_test.go similarity index 81% rename from internal/lsp/regtest/env_test.go rename to gopls/internal/lsp/regtest/env_test.go index fe5864ca77c..824c602df4d 100644 --- a/internal/lsp/regtest/env_test.go +++ b/gopls/internal/lsp/regtest/env_test.go @@ -9,11 +9,11 @@ import ( "encoding/json" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func TestProgressUpdating(t *testing.T) { - e := &Env{ + a := &Awaiter{ state: State{ outstandingWork: make(map[protocol.ProgressToken]*workProgress), startedWork: make(map[string]uint64), @@ -21,12 +21,12 @@ func TestProgressUpdating(t *testing.T) { }, } ctx := context.Background() - if err := e.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ + if err := a.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ Token: "foo", }); err != nil { t.Fatal(err) } - if err := e.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ + if err := a.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ Token: "bar", }); err != nil { t.Fatal(err) @@ -53,14 +53,14 @@ func TestProgressUpdating(t *testing.T) { if err := json.Unmarshal(data, &unmarshaled); err != nil { t.Fatal(err) } - if err := e.onProgress(ctx, &unmarshaled); err != nil { + if err := a.onProgress(ctx, &unmarshaled); err != nil { t.Fatal(err) } } - if _, ok := e.state.outstandingWork["foo"]; ok { + if _, ok := a.state.outstandingWork["foo"]; ok { t.Error("got work entry for \"foo\", want none") } - got := *e.state.outstandingWork["bar"] + got := *a.state.outstandingWork["bar"] want := workProgress{title: "bar work", percent: 42} if got != want { t.Errorf("work progress for \"bar\": %v, want %v", got, want) diff --git a/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go similarity index 76% rename from internal/lsp/regtest/expectation.go rename to gopls/internal/lsp/regtest/expectation.go index ab808f9e8cf..d09398779c1 100644 --- a/internal/lsp/regtest/expectation.go +++ b/gopls/internal/lsp/regtest/expectation.go @@ -7,11 +7,12 @@ package regtest import ( "fmt" "regexp" + "sort" "strings" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -130,6 +131,33 @@ func AnyOf(anyOf ...Expectation) *SimpleExpectation { } } +// AllOf expects that all given expectations are met. +// +// TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf +// and AllOf) is that we lose the information of *why* they failed: the Awaiter +// is not smart enough to look inside. +// +// Refactor the API such that the Check function is responsible for explaining +// why an expectation failed. This should allow us to significantly improve +// test output: we won't need to summarize state at all, as the verdict +// explanation itself should describe clearly why the expectation not met. +func AllOf(allOf ...Expectation) *SimpleExpectation { + check := func(s State) Verdict { + verdict := Met + for _, e := range allOf { + if v := e.Check(s); v > verdict { + verdict = v + } + } + return verdict + } + description := describeExpectations(allOf...) + return &SimpleExpectation{ + check: check, + description: fmt.Sprintf("All of:\n%s", description), + } +} + // ReadDiagnostics is an 'expectation' that is used to read diagnostics // atomically. It is intended to be used with 'OnceMet'. func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) *SimpleExpectation { @@ -162,17 +190,19 @@ func NoOutstandingWork() SimpleExpectation { } } -// NoShowMessage asserts that the editor has not received a ShowMessage. -func NoShowMessage() SimpleExpectation { +// NoShownMessage asserts that the editor has not received a ShowMessage. +func NoShownMessage(subString string) SimpleExpectation { check := func(s State) Verdict { - if len(s.showMessage) == 0 { - return Met + for _, m := range s.showMessage { + if strings.Contains(m.Message, subString) { + return Unmeetable + } } - return Unmeetable + return Met } return SimpleExpectation{ check: check, - description: "no ShowMessage received", + description: fmt.Sprintf("no ShowMessage received containing %q", subString), } } @@ -216,6 +246,54 @@ func ShowMessageRequest(title string) SimpleExpectation { } } +// DoneDiagnosingChanges expects that diagnostics are complete from common +// change notifications: didOpen, didChange, didSave, didChangeWatchedFiles, +// and didClose. +// +// This can be used when multiple notifications may have been sent, such as +// when a didChange is immediately followed by a didSave. It is insufficient to +// simply await NoOutstandingWork, because the LSP client has no control over +// when the server starts processing a notification. Therefore, we must keep +// track of +func (e *Env) DoneDiagnosingChanges() Expectation { + stats := e.Editor.Stats() + statsBySource := map[lsp.ModificationSource]uint64{ + lsp.FromDidOpen: stats.DidOpen, + lsp.FromDidChange: stats.DidChange, + lsp.FromDidSave: stats.DidSave, + lsp.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles, + lsp.FromDidClose: stats.DidClose, + } + + var expected []lsp.ModificationSource + for k, v := range statsBySource { + if v > 0 { + expected = append(expected, k) + } + } + + // Sort for stability. + sort.Slice(expected, func(i, j int) bool { + return expected[i] < expected[j] + }) + + var all []Expectation + for _, source := range expected { + all = append(all, CompletedWork(lsp.DiagnosticWorkTitle(source), statsBySource[source], true)) + } + + return AllOf(all...) +} + +// AfterChange expects that the given expectations will be met after all +// state-changing notifications have been processed by the server. +func (e *Env) AfterChange(expectations ...Expectation) Expectation { + return OnceMet( + e.DoneDiagnosingChanges(), + expectations..., + ) +} + // DoneWithOpen expects all didOpen notifications currently sent by the editor // to be completely processed. func (e *Env) DoneWithOpen() Expectation { @@ -223,10 +301,11 @@ func (e *Env) DoneWithOpen() Expectation { return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens, true) } -// StartedChange expects there to have been i work items started for -// processing didChange notifications. -func StartedChange(i uint64) Expectation { - return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), i) +// StartedChange expects that the server has at least started processing all +// didChange notifications sent from the client. +func (e *Env) StartedChange() Expectation { + changes := e.Editor.Stats().DidChange + return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes) } // DoneWithChange expects all didChange notifications currently sent by the @@ -308,7 +387,7 @@ func OutstandingWork(title, msg string) SimpleExpectation { } return SimpleExpectation{ check: check, - description: fmt.Sprintf("outstanding work: %s", title), + description: fmt.Sprintf("outstanding work: %q containing %q", title, msg), } } @@ -336,7 +415,11 @@ func NoErrorLogs() LogExpectation { } // LogMatching asserts that the client has received a log message -// of type typ matching the regexp re. +// of type typ matching the regexp re a certain number of times. +// +// The count argument specifies the expected number of matching logs. If +// atLeast is set, this is a lower bound, otherwise there must be exactly cound +// matching logs. func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) LogExpectation { rec, err := regexp.Compile(re) if err != nil { @@ -394,32 +477,66 @@ func NoLogMatching(typ protocol.MessageType, re string) LogExpectation { } } -// RegistrationExpectation is an expectation on the capability registrations -// received by the editor from gopls. -type RegistrationExpectation struct { - check func([]*protocol.RegistrationParams) Verdict - description string +// FileWatchMatching expects that a file registration matches re. +func FileWatchMatching(re string) SimpleExpectation { + return SimpleExpectation{ + check: checkFileWatch(re, Met, Unmet), + description: fmt.Sprintf("file watch matching %q", re), + } } -// Check implements the Expectation interface. -func (e RegistrationExpectation) Check(s State) Verdict { - return e.check(s.registrations) +// NoFileWatchMatching expects that no file registration matches re. +func NoFileWatchMatching(re string) SimpleExpectation { + return SimpleExpectation{ + check: checkFileWatch(re, Unmet, Met), + description: fmt.Sprintf("no file watch matching %q", re), + } } -// Description implements the Expectation interface. -func (e RegistrationExpectation) Description() string { - return e.description +func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict { + rec := regexp.MustCompile(re) + return func(s State) Verdict { + r := s.registeredCapabilities["workspace/didChangeWatchedFiles"] + watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{}) + for _, watcher := range watchers { + pattern := jsonProperty(watcher, "globPattern").(string) + if rec.MatchString(pattern) { + return onMatch + } + } + return onNoMatch + } +} + +// jsonProperty extracts a value from a path of JSON property names, assuming +// the default encoding/json unmarshaling to the empty interface (i.e.: that +// JSON objects are unmarshalled as map[string]interface{}) +// +// For example, if obj is unmarshalled from the following json: +// +// { +// "foo": { "bar": 3 } +// } +// +// Then jsonProperty(obj, "foo", "bar") will be 3. +func jsonProperty(obj interface{}, path ...string) interface{} { + if len(path) == 0 || obj == nil { + return obj + } + m := obj.(map[string]interface{}) + return jsonProperty(m[path[0]], path[1:]...) } // RegistrationMatching asserts that the client has received a capability // registration matching the given regexp. -func RegistrationMatching(re string) RegistrationExpectation { - rec, err := regexp.Compile(re) - if err != nil { - panic(err) - } - check := func(params []*protocol.RegistrationParams) Verdict { - for _, p := range params { +// +// TODO(rfindley): remove this once TestWatchReplaceTargets has been revisited. +// +// Deprecated: use (No)FileWatchMatching +func RegistrationMatching(re string) SimpleExpectation { + rec := regexp.MustCompile(re) + check := func(s State) Verdict { + for _, p := range s.registrations { for _, r := range p.Registrations { if rec.Match([]byte(r.Method)) { return Met @@ -428,38 +545,18 @@ func RegistrationMatching(re string) RegistrationExpectation { } return Unmet } - return RegistrationExpectation{ + return SimpleExpectation{ check: check, description: fmt.Sprintf("registration matching %q", re), } } -// UnregistrationExpectation is an expectation on the capability -// unregistrations received by the editor from gopls. -type UnregistrationExpectation struct { - check func([]*protocol.UnregistrationParams) Verdict - description string -} - -// Check implements the Expectation interface. -func (e UnregistrationExpectation) Check(s State) Verdict { - return e.check(s.unregistrations) -} - -// Description implements the Expectation interface. -func (e UnregistrationExpectation) Description() string { - return e.description -} - // UnregistrationMatching asserts that the client has received an // unregistration whose ID matches the given regexp. -func UnregistrationMatching(re string) UnregistrationExpectation { - rec, err := regexp.Compile(re) - if err != nil { - panic(err) - } - check := func(params []*protocol.UnregistrationParams) Verdict { - for _, p := range params { +func UnregistrationMatching(re string) SimpleExpectation { + rec := regexp.MustCompile(re) + check := func(s State) Verdict { + for _, p := range s.unregistrations { for _, r := range p.Unregisterations { if rec.Match([]byte(r.Method)) { return Met @@ -468,7 +565,7 @@ func UnregistrationMatching(re string) UnregistrationExpectation { } return Unmet } - return UnregistrationExpectation{ + return SimpleExpectation{ check: check, description: fmt.Sprintf("unregistration matching %q", re), } @@ -613,25 +710,7 @@ func NoDiagnostics(name string) Expectation { } return SimpleExpectation{ check: check, - description: "no diagnostics", - } -} - -// AnyDiagnosticAtCurrentVersion asserts that there is a diagnostic report for -// the current edited version of the buffer corresponding to the given -// workdir-relative pathname. -func (e *Env) AnyDiagnosticAtCurrentVersion(name string) Expectation { - version := e.Editor.BufferVersion(name) - check := func(s State) Verdict { - diags, ok := s.diagnostics[name] - if ok && diags.Version == int32(version) { - return Met - } - return Unmet - } - return SimpleExpectation{ - check: check, - description: fmt.Sprintf("any diagnostics at version %d", version), + description: fmt.Sprintf("no diagnostics for %q", name), } } @@ -677,14 +756,6 @@ func (e *Env) NoDiagnosticAtRegexp(name, re string) DiagnosticExpectation { return DiagnosticExpectation{path: name, pos: &pos, re: re, present: false} } -// NoDiagnosticAt asserts that there is no diagnostic entry at the position -// specified by line and col, for the workdir-relative path name. -// This should only be used in combination with OnceMet for a given condition, -// otherwise it may always succeed. -func NoDiagnosticAt(name string, line, col int) DiagnosticExpectation { - return DiagnosticExpectation{path: name, pos: &fake.Pos{Line: line, Column: col}, present: false} -} - // NoDiagnosticWithMessage asserts that there is no diagnostic entry with the // given message. // diff --git a/internal/lsp/regtest/regtest.go b/gopls/internal/lsp/regtest/regtest.go similarity index 62% rename from internal/lsp/regtest/regtest.go rename to gopls/internal/lsp/regtest/regtest.go index 9ebc673f8c0..b2ef3575e5d 100644 --- a/internal/lsp/regtest/regtest.go +++ b/gopls/internal/lsp/regtest/regtest.go @@ -8,14 +8,17 @@ import ( "context" "flag" "fmt" + "go/token" "io/ioutil" "os" "runtime" "testing" "time" - "golang.org/x/tools/internal/lsp/cmd" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/gocommand" + "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/tool" ) @@ -77,38 +80,37 @@ func (r RunMultiple) Run(t *testing.T, files string, f TestFunc) { } } -// The regtests run significantly slower on these operating systems, due to (we -// believe) kernel locking behavior. Only run in singleton mode on these -// operating system when using -short. -var slowGOOS = map[string]bool{ - "darwin": true, - "openbsd": true, - "plan9": true, -} - +// DefaultModes returns the default modes to run for each regression test (they +// may be reconfigured by the tests themselves). func DefaultModes() Mode { - normal := Singleton | Experimental - if slowGOOS[runtime.GOOS] && testing.Short() { - normal = Singleton + modes := Default + if !testing.Short() { + modes |= Experimental | Forwarded } if *runSubprocessTests { - return normal | SeparateProcess + modes |= SeparateProcess } - return normal + return modes } // Main sets up and tears down the shared regtest state. func Main(m *testing.M, hook func(*source.Options)) { + // golang/go#54461: enable additional debugging around hanging Go commands. + gocommand.DebugHangingGoCommands = true + + // If this magic environment variable is set, run gopls instead of the test + // suite. See the documentation for runTestAsGoplsEnvvar for more details. + if os.Getenv(runTestAsGoplsEnvvar) == "true" { + tool.Main(context.Background(), cmd.New("gopls", "", nil, hook), os.Args[1:]) + os.Exit(0) + } + testenv.ExitIfSmallMachine() // Disable GOPACKAGESDRIVER, as it can cause spurious test failures. os.Setenv("GOPACKAGESDRIVER", "off") flag.Parse() - if os.Getenv("_GOPLS_TEST_BINARY_RUN_AS_GOPLS") == "true" { - tool.Main(context.Background(), cmd.New("gopls", "", nil, nil), os.Args[1:]) - os.Exit(0) - } runner = &Runner{ DefaultModes: DefaultModes(), @@ -116,34 +118,38 @@ func Main(m *testing.M, hook func(*source.Options)) { PrintGoroutinesOnFailure: *printGoroutinesOnFailure, SkipCleanup: *skipCleanup, OptionsHook: hook, + fset: token.NewFileSet(), + store: memoize.NewStore(memoize.NeverEvict), } - if *runSubprocessTests { - goplsPath := *goplsBinaryPath - if goplsPath == "" { - var err error - goplsPath, err = os.Executable() - if err != nil { - panic(fmt.Sprintf("finding test binary path: %v", err)) - } + + runner.goplsPath = *goplsBinaryPath + if runner.goplsPath == "" { + var err error + runner.goplsPath, err = os.Executable() + if err != nil { + panic(fmt.Sprintf("finding test binary path: %v", err)) } - runner.GoplsPath = goplsPath } + dir, err := ioutil.TempDir("", "gopls-regtest-") if err != nil { panic(fmt.Errorf("creating regtest temp directory: %v", err)) } - runner.TempDir = dir - - code := m.Run() - if err := runner.Close(); err != nil { - fmt.Fprintf(os.Stderr, "closing test runner: %v\n", err) - // Regtest cleanup is broken in go1.12 and earlier, and sometimes flakes on - // Windows due to file locking, but this is OK for our CI. - // - // Fail on go1.13+, except for windows and android which have shutdown problems. - if testenv.Go1Point() >= 13 && runtime.GOOS != "windows" && runtime.GOOS != "android" { - os.Exit(1) + runner.tempDir = dir + + var code int + defer func() { + if err := runner.Close(); err != nil { + fmt.Fprintf(os.Stderr, "closing test runner: %v\n", err) + // Regtest cleanup is broken in go1.12 and earlier, and sometimes flakes on + // Windows due to file locking, but this is OK for our CI. + // + // Fail on go1.13+, except for windows and android which have shutdown problems. + if testenv.Go1Point() >= 13 && runtime.GOOS != "windows" && runtime.GOOS != "android" { + os.Exit(1) + } } - } - os.Exit(code) + os.Exit(code) + }() + code = m.Run() } diff --git a/gopls/internal/lsp/regtest/runner.go b/gopls/internal/lsp/regtest/runner.go new file mode 100644 index 00000000000..effc8aae4b2 --- /dev/null +++ b/gopls/internal/lsp/regtest/runner.go @@ -0,0 +1,538 @@ +// 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 regtest + +import ( + "bytes" + "context" + "fmt" + "go/token" + "io" + "io/ioutil" + "net" + "os" + "path/filepath" + "runtime" + "runtime/pprof" + "strings" + "sync" + "testing" + "time" + + exec "golang.org/x/sys/execabs" + + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/memoize" + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/xcontext" +) + +// Mode is a bitmask that defines for which execution modes a test should run. +// +// Each mode controls several aspects of gopls' configuration: +// - Which server options to use for gopls sessions +// - Whether to use a shared cache +// - Whether to use a shared server +// - Whether to run the server in-process or in a separate process +// +// The behavior of each mode with respect to these aspects is summarized below. +// TODO(rfindley, cleanup): rather than using arbitrary names for these modes, +// we can compose them explicitly out of the features described here, allowing +// individual tests more freedom in constructing problematic execution modes. +// For example, a test could assert on a certain behavior when running with +// experimental options on a separate process. Moreover, we could unify 'Modes' +// with 'Options', and use RunMultiple rather than a hard-coded loop through +// modes. +// +// Mode | Options | Shared Cache? | Shared Server? | In-process? +// --------------------------------------------------------------------------- +// Default | Default | Y | N | Y +// Forwarded | Default | Y | Y | Y +// SeparateProcess | Default | Y | Y | N +// Experimental | Experimental | N | N | Y +type Mode int + +const ( + // Default mode runs gopls with the default options, communicating over pipes + // to emulate the lsp sidecar execution mode, which communicates over + // stdin/stdout. + // + // It uses separate servers for each test, but a shared cache, to avoid + // duplicating work when processing GOROOT. + Default Mode = 1 << iota + + // Forwarded uses the default options, but forwards connections to a shared + // in-process gopls server. + Forwarded + + // SeparateProcess uses the default options, but forwards connection to an + // external gopls daemon. + // + // Only supported on GOOS=linux. + SeparateProcess + + // Experimental enables all of the experimental configurations that are + // being developed, and runs gopls in sidecar mode. + // + // It uses a separate cache for each test, to exercise races that may only + // appear with cache misses. + Experimental +) + +func (m Mode) String() string { + switch m { + case Default: + return "default" + case Forwarded: + return "forwarded" + case SeparateProcess: + return "separate process" + case Experimental: + return "experimental" + default: + return "unknown mode" + } +} + +// A Runner runs tests in gopls execution environments, as specified by its +// modes. For modes that share state (for example, a shared cache or common +// remote), any tests that execute on the same Runner will share the same +// state. +type Runner struct { + // Configuration + DefaultModes Mode // modes to run for each test + Timeout time.Duration // per-test timeout, if set + PrintGoroutinesOnFailure bool // whether to dump goroutines on test failure + SkipCleanup bool // if set, don't delete test data directories when the test exits + OptionsHook func(*source.Options) // if set, use these options when creating gopls sessions + + // Immutable state shared across test invocations + goplsPath string // path to the gopls executable (for SeparateProcess mode) + tempDir string // shared parent temp directory + fset *token.FileSet // shared FileSet + store *memoize.Store // shared store + + // Lazily allocated resources + tsOnce sync.Once + ts *servertest.TCPServer // shared in-process test server ("forwarded" mode) + + startRemoteOnce sync.Once + remoteSocket string // unix domain socket for shared daemon ("separate process" mode) + remoteErr error + cancelRemote func() +} + +type runConfig struct { + editor fake.EditorConfig + sandbox fake.SandboxConfig + modes Mode + skipHooks bool +} + +// A RunOption augments the behavior of the test runner. +type RunOption interface { + set(*runConfig) +} + +type optionSetter func(*runConfig) + +func (f optionSetter) set(opts *runConfig) { + f(opts) +} + +// ProxyFiles configures a file proxy using the given txtar-encoded string. +func ProxyFiles(txt string) RunOption { + return optionSetter(func(opts *runConfig) { + opts.sandbox.ProxyFiles = fake.UnpackTxt(txt) + }) +} + +// Modes configures the execution modes that the test should run in. +// +// By default, modes are configured by the test runner. If this option is set, +// it overrides the set of default modes and the test runs in exactly these +// modes. +func Modes(modes Mode) RunOption { + return optionSetter(func(opts *runConfig) { + if opts.modes != 0 { + panic("modes set more than once") + } + opts.modes = modes + }) +} + +// WindowsLineEndings configures the editor to use windows line endings. +func WindowsLineEndings() RunOption { + return optionSetter(func(opts *runConfig) { + opts.editor.WindowsLineEndings = true + }) +} + +// Settings is a RunOption that sets user-provided configuration for the LSP +// server. +// +// As a special case, the env setting must not be provided via Settings: use +// EnvVars instead. +type Settings map[string]interface{} + +func (s Settings) set(opts *runConfig) { + if opts.editor.Settings == nil { + opts.editor.Settings = make(map[string]interface{}) + } + for k, v := range s { + opts.editor.Settings[k] = v + } +} + +// WorkspaceFolders configures the workdir-relative workspace folders to send +// to the LSP server. By default the editor sends a single workspace folder +// corresponding to the workdir root. To explicitly configure no workspace +// folders, use WorkspaceFolders with no arguments. +func WorkspaceFolders(relFolders ...string) RunOption { + if len(relFolders) == 0 { + // Use an empty non-nil slice to signal explicitly no folders. + relFolders = []string{} + } + return optionSetter(func(opts *runConfig) { + opts.editor.WorkspaceFolders = relFolders + }) +} + +// EnvVars sets environment variables for the LSP session. When applying these +// variables to the session, the special string $SANDBOX_WORKDIR is replaced by +// the absolute path to the sandbox working directory. +type EnvVars map[string]string + +func (e EnvVars) set(opts *runConfig) { + if opts.editor.Env == nil { + opts.editor.Env = make(map[string]string) + } + for k, v := range e { + opts.editor.Env[k] = v + } +} + +// InGOPATH configures the workspace working directory to be GOPATH, rather +// than a separate working directory for use with modules. +func InGOPATH() RunOption { + return optionSetter(func(opts *runConfig) { + opts.sandbox.InGoPath = true + }) +} + +type TestFunc func(t *testing.T, env *Env) + +// Run executes the test function in the default configured gopls execution +// modes. For each a test run, a new workspace is created containing the +// un-txtared files specified by filedata. +func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) { + // TODO(rfindley): this function has gotten overly complicated, and warrants + // refactoring. + t.Helper() + checkBuilder(t) + + tests := []struct { + name string + mode Mode + getServer func(func(*source.Options)) jsonrpc2.StreamServer + }{ + {"default", Default, r.defaultServer}, + {"forwarded", Forwarded, r.forwardedServer}, + {"separate_process", SeparateProcess, r.separateProcessServer}, + {"experimental", Experimental, r.experimentalServer}, + } + + for _, tc := range tests { + tc := tc + var config runConfig + for _, opt := range opts { + opt.set(&config) + } + modes := r.DefaultModes + if config.modes != 0 { + modes = config.modes + } + if modes&tc.mode == 0 { + continue + } + + t.Run(tc.name, func(t *testing.T) { + // TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak + // goroutines in this test function. + // stacktest.NoLeak(t) + + ctx := context.Background() + if r.Timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, r.Timeout) + defer cancel() + } else if d, ok := testenv.Deadline(t); ok { + timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup. + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + + // TODO(rfindley): do we need an instance at all? Can it be removed? + ctx = debug.WithInstance(ctx, "", "off") + + rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name())) + if err := os.MkdirAll(rootDir, 0755); err != nil { + t.Fatal(err) + } + + files := fake.UnpackTxt(files) + if config.editor.WindowsLineEndings { + for name, data := range files { + files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")) + } + } + config.sandbox.Files = files + config.sandbox.RootDir = rootDir + sandbox, err := fake.NewSandbox(&config.sandbox) + if err != nil { + t.Fatal(err) + } + defer func() { + if !r.SkipCleanup { + if err := sandbox.Close(); err != nil { + pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) + t.Errorf("closing the sandbox: %v", err) + } + } + }() + + ss := tc.getServer(r.OptionsHook) + + framer := jsonrpc2.NewRawStream + ls := &loggingFramer{} + framer = ls.framer(jsonrpc2.NewRawStream) + ts := servertest.NewPipeServer(ss, framer) + + awaiter := NewAwaiter(sandbox.Workdir) + editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks()) + if err != nil { + t.Fatal(err) + } + env := &Env{ + T: t, + Ctx: ctx, + Sandbox: sandbox, + Editor: editor, + Server: ts, + Awaiter: awaiter, + } + defer func() { + if t.Failed() && r.PrintGoroutinesOnFailure { + pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) + } + if t.Failed() || *printLogs { + ls.printBuffers(t.Name(), os.Stderr) + } + // For tests that failed due to a timeout, don't fail to shutdown + // because ctx is done. + // + // There is little point to setting an arbitrary timeout for closing + // the editor: in general we want to clean up before proceeding to the + // next test, and if there is a deadlock preventing closing it will + // eventually be handled by the `go test` timeout. + if err := editor.Close(xcontext.Detach(ctx)); err != nil { + t.Errorf("closing editor: %v", err) + } + }() + // Always await the initial workspace load. + env.Await(InitialWorkspaceLoad) + test(t, env) + }) + } +} + +// longBuilders maps builders that are skipped when -short is set to a +// (possibly empty) justification. +var longBuilders = map[string]string{ + "openbsd-amd64-64": "golang.org/issues/42789", + "openbsd-386-64": "golang.org/issues/42789", + "openbsd-386-68": "golang.org/issues/42789", + "openbsd-amd64-68": "golang.org/issues/42789", + "darwin-amd64-10_12": "", + "freebsd-amd64-race": "", + "illumos-amd64": "", + "netbsd-arm-bsiegert": "", + "solaris-amd64-oraclerel": "", + "windows-arm-zx2c4": "", +} + +func checkBuilder(t *testing.T) { + t.Helper() + builder := os.Getenv("GO_BUILDER_NAME") + if reason, ok := longBuilders[builder]; ok && testing.Short() { + if reason != "" { + t.Skipf("Skipping %s with -short due to %s", builder, reason) + } else { + t.Skipf("Skipping %s with -short", builder) + } + } +} + +type loggingFramer struct { + mu sync.Mutex + buf *safeBuffer +} + +// safeBuffer is a threadsafe buffer for logs. +type safeBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (b *safeBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer { + return func(nc net.Conn) jsonrpc2.Stream { + s.mu.Lock() + framed := false + if s.buf == nil { + s.buf = &safeBuffer{buf: bytes.Buffer{}} + framed = true + } + s.mu.Unlock() + stream := f(nc) + if framed { + return protocol.LoggingStream(stream, s.buf) + } + return stream + } +} + +func (s *loggingFramer) printBuffers(testname string, w io.Writer) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.buf == nil { + return + } + fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname) + s.buf.mu.Lock() + io.Copy(w, &s.buf.buf) + s.buf.mu.Unlock() + fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) +} + +// defaultServer handles the Default execution mode. +func (r *Runner) defaultServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { + return lsprpc.NewStreamServer(cache.New(r.fset, r.store, optsHook), false) +} + +// experimentalServer handles the Experimental execution mode. +func (r *Runner) experimentalServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { + options := func(o *source.Options) { + optsHook(o) + o.EnableAllExperiments() + // ExperimentalWorkspaceModule is not (as of writing) enabled by + // source.Options.EnableAllExperiments, but we want to test it. + o.ExperimentalWorkspaceModule = true + } + return lsprpc.NewStreamServer(cache.New(nil, nil, options), false) +} + +// forwardedServer handles the Forwarded execution mode. +func (r *Runner) forwardedServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { + r.tsOnce.Do(func() { + ctx := context.Background() + ctx = debug.WithInstance(ctx, "", "off") + ss := lsprpc.NewStreamServer(cache.New(nil, nil, optsHook), false) + r.ts = servertest.NewTCPServer(ctx, ss, nil) + }) + return newForwarder("tcp", r.ts.Addr) +} + +// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running +// tests. It's a trick to allow tests to find a binary to use to start a gopls +// subprocess. +const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS" + +// separateProcessServer handles the SeparateProcess execution mode. +func (r *Runner) separateProcessServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { + if runtime.GOOS != "linux" { + panic("separate process execution mode is only supported on linux") + } + + r.startRemoteOnce.Do(func() { + socketDir, err := ioutil.TempDir(r.tempDir, "gopls-regtest-socket") + if err != nil { + r.remoteErr = err + return + } + r.remoteSocket = filepath.Join(socketDir, "gopls-test-daemon") + + // The server should be killed by when the test runner exits, but to be + // conservative also set a listen timeout. + args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"} + + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, r.goplsPath, args...) + cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") + + // Start the external gopls process. This is still somewhat racy, as we + // don't know when gopls binds to the socket, but the gopls forwarder + // client has built-in retry behavior that should mostly mitigate this + // problem (and if it doesn't, we probably want to improve the retry + // behavior). + if err := cmd.Start(); err != nil { + cancel() + r.remoteSocket = "" + r.remoteErr = err + } else { + r.cancelRemote = cancel + // Spin off a goroutine to wait, so that we free up resources when the + // server exits. + go cmd.Wait() + } + }) + + return newForwarder("unix", r.remoteSocket) +} + +func newForwarder(network, address string) *lsprpc.Forwarder { + server, err := lsprpc.NewForwarder(network+";"+address, nil) + if err != nil { + // This should never happen, as we are passing an explicit address. + panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err)) + } + return server +} + +// Close cleans up resource that have been allocated to this workspace. +func (r *Runner) Close() error { + var errmsgs []string + if r.ts != nil { + if err := r.ts.Close(); err != nil { + errmsgs = append(errmsgs, err.Error()) + } + } + if r.cancelRemote != nil { + r.cancelRemote() + } + if !r.SkipCleanup { + if err := os.RemoveAll(r.tempDir); err != nil { + errmsgs = append(errmsgs, err.Error()) + } + } + if len(errmsgs) > 0 { + return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t")) + } + return nil +} diff --git a/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go similarity index 85% rename from internal/lsp/regtest/wrappers.go rename to gopls/internal/lsp/regtest/wrappers.go index 9031e71f1f1..13e5f7bc819 100644 --- a/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -7,11 +7,10 @@ package regtest import ( "encoding/json" "path" - "testing" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func (e *Env) ChangeFilesOnDisk(events []fake.FileEvent) { @@ -59,6 +58,17 @@ func (e *Env) WriteWorkspaceFiles(files map[string]string) { } } +// ListFiles lists relative paths to files in the given directory. +// It calls t.Fatal on any error. +func (e *Env) ListFiles(dir string) []string { + e.T.Helper() + paths, err := e.Sandbox.Workdir.ListFiles(dir) + if err != nil { + e.T.Fatal(err) + } + return paths +} + // OpenFile opens a file in the editor, calling t.Fatal on any error. func (e *Env) OpenFile(name string) { e.T.Helper() @@ -278,6 +288,17 @@ func (e *Env) RunGoCommandInDir(dir, verb string, args ...string) { } } +// GoVersion checks the version of the go command. +// It returns the X in Go 1.X. +func (e *Env) GoVersion() int { + e.T.Helper() + v, err := e.Sandbox.GoVersion(e.Ctx) + if err != nil { + e.T.Fatal(err) + } + return v +} + // DumpGoSum prints the correct go.sum contents for dir in txtar format, // for use in creating regtests. func (e *Env) DumpGoSum(dir string) { @@ -358,6 +379,17 @@ func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result inter } } +// InlayHints calls textDocument/inlayHints for the given path, calling t.Fatal on +// any error. +func (e *Env) InlayHints(path string) []protocol.InlayHint { + e.T.Helper() + hints, err := e.Editor.InlayHint(e.Ctx, path) + if err != nil { + e.T.Fatal(err) + } + return hints +} + // WorkspaceSymbol calls workspace/symbol func (e *Env) WorkspaceSymbol(sym string) []protocol.SymbolInformation { e.T.Helper() @@ -368,8 +400,7 @@ func (e *Env) WorkspaceSymbol(sym string) []protocol.SymbolInformation { return ans } -// References calls textDocument/references for the given path at the given -// position. +// References wraps Editor.References, calling t.Fatal on any error. func (e *Env) References(path string, pos fake.Pos) []protocol.Location { e.T.Helper() locations, err := e.Editor.References(e.Ctx, path, pos) @@ -379,6 +410,7 @@ func (e *Env) References(path string, pos fake.Pos) []protocol.Location { return locations } +// Rename wraps Editor.Rename, calling t.Fatal on any error. func (e *Env) Rename(path string, pos fake.Pos, newName string) { e.T.Helper() if err := e.Editor.Rename(e.Ctx, path, pos, newName); err != nil { @@ -386,6 +418,24 @@ func (e *Env) Rename(path string, pos fake.Pos, newName string) { } } +// Implementations wraps Editor.Implementations, calling t.Fatal on any error. +func (e *Env) Implementations(path string, pos fake.Pos) []protocol.Location { + e.T.Helper() + locations, err := e.Editor.Implementations(e.Ctx, path, pos) + if err != nil { + e.T.Fatal(err) + } + return locations +} + +// RenameFile wraps Editor.RenameFile, calling t.Fatal on any error. +func (e *Env) RenameFile(oldPath, newPath string) { + e.T.Helper() + if err := e.Editor.RenameFile(e.Ctx, oldPath, newPath); err != nil { + e.T.Fatal(err) + } +} + // Completion executes a completion request on the server. func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList { e.T.Helper() @@ -416,31 +466,19 @@ func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []proto return actions } -func (e *Env) ChangeConfiguration(t *testing.T, config *fake.EditorConfig) { - e.Editor.Config = *config - if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{ - // gopls currently ignores the Settings field - }); err != nil { - t.Fatal(err) +// ChangeConfiguration updates the editor config, calling t.Fatal on any error. +func (e *Env) ChangeConfiguration(newConfig fake.EditorConfig) { + e.T.Helper() + if err := e.Editor.ChangeConfiguration(e.Ctx, newConfig); err != nil { + e.T.Fatal(err) } } -// ChangeEnv modifies the editor environment and reconfigures the LSP client. -// TODO: extend this to "ChangeConfiguration", once we refactor the way editor -// configuration is defined. -func (e *Env) ChangeEnv(overlay map[string]string) { +// ChangeWorkspaceFolders updates the editor workspace folders, calling t.Fatal +// on any error. +func (e *Env) ChangeWorkspaceFolders(newFolders ...string) { e.T.Helper() - // TODO: to be correct, this should probably be synchronized, but right now - // configuration is only ever modified synchronously in a regtest, so this - // correctness can wait for the previously mentioned refactoring. - if e.Editor.Config.Env == nil { - e.Editor.Config.Env = make(map[string]string) - } - for k, v := range overlay { - e.Editor.Config.Env[k] = v - } - var params protocol.DidChangeConfigurationParams - if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, ¶ms); err != nil { + if err := e.Editor.ChangeWorkspaceFolders(e.Ctx, newFolders); err != nil { e.T.Fatal(err) } } diff --git a/internal/lsp/rename.go b/gopls/internal/lsp/rename.go similarity index 53% rename from internal/lsp/rename.go rename to gopls/internal/lsp/rename.go index 739ae906b37..e9bb2d40033 100644 --- a/internal/lsp/rename.go +++ b/gopls/internal/lsp/rename.go @@ -6,9 +6,11 @@ package lsp import ( "context" + "path/filepath" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { @@ -17,12 +19,15 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr if !ok { return nil, err } - edits, err := source.Rename(ctx, snapshot, fh, params.Position, params.NewName) + // Because we don't handle directory renaming within source.Rename, source.Rename returns + // boolean value isPkgRenaming to determine whether an DocumentChanges of type RenameFile should + // be added to the return protocol.WorkspaceEdit value. + edits, isPkgRenaming, err := source.Rename(ctx, snapshot, fh, params.Position, params.NewName) if err != nil { return nil, err } - var docChanges []protocol.TextDocumentEdit + var docChanges []protocol.DocumentChanges for uri, e := range edits { fh, err := snapshot.GetVersionedFile(ctx, uri) if err != nil { @@ -30,11 +35,29 @@ func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*pr } docChanges = append(docChanges, documentChanges(fh, e)...) } + if isPkgRenaming { + uri := params.TextDocument.URI.SpanURI() + oldBase := filepath.Dir(span.URI.Filename(uri)) + newURI := filepath.Join(filepath.Dir(oldBase), params.NewName) + docChanges = append(docChanges, protocol.DocumentChanges{ + RenameFile: &protocol.RenameFile{ + Kind: "rename", + OldURI: protocol.URIFromPath(oldBase), + NewURI: protocol.URIFromPath(newURI), + }, + }) + } return &protocol.WorkspaceEdit{ DocumentChanges: docChanges, }, nil } +// prepareRename implements the textDocument/prepareRename handler. It may +// return (nil, nil) if there is no rename at the cursor position, but it is +// not desirable to display an error to the user. +// +// TODO(rfindley): why wouldn't we want to show an error to the user, if the +// user initiated a rename request at the cursor? func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.PrepareRename2Gn, error) { snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) defer release() diff --git a/gopls/internal/lsp/reset_golden.sh b/gopls/internal/lsp/reset_golden.sh new file mode 100755 index 00000000000..ff7f4d08208 --- /dev/null +++ b/gopls/internal/lsp/reset_golden.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# Copyright 2022 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +# +# Updates the *.golden files ... to match the tests' current behavior. + +set -eu + +GO117BIN="go1.17.9" + +command -v $GO117BIN >/dev/null 2>&1 || { + go install golang.org/dl/$GO117BIN@latest + $GO117BIN download +} + +find ./internal/lsp/testdata -name *.golden ! -name summary*.txt.golden -delete +# Here we intentionally do not run the ./internal/lsp/source tests with +# -golden. Eventually these tests will be deleted, and in the meantime they are +# redundant with the ./internal/lsp tests. +# +# Note: go1.17.9 tests must be run *before* go tests, as by convention the +# golden output should match the output of gopls built with the most recent +# version of Go. If output differs at 1.17, tests must be tolerant of the 1.17 +# output. +$GO117BIN test ./internal/lsp -golden +go test ./internal/lsp -golden +$GO117BIN test ./test -golden +go test ./test -golden diff --git a/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go similarity index 100% rename from internal/lsp/safetoken/safetoken.go rename to gopls/internal/lsp/safetoken/safetoken.go diff --git a/internal/lsp/safetoken/safetoken_test.go b/gopls/internal/lsp/safetoken/safetoken_test.go similarity index 84% rename from internal/lsp/safetoken/safetoken_test.go rename to gopls/internal/lsp/safetoken/safetoken_test.go index 43d73a74d78..1486d68f327 100644 --- a/internal/lsp/safetoken/safetoken_test.go +++ b/gopls/internal/lsp/safetoken/safetoken_test.go @@ -21,7 +21,7 @@ func TestTokenOffset(t *testing.T) { pkgs, err := packages.Load(&packages.Config{ Fset: fset, Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, - }, "go/token", "golang.org/x/tools/internal/lsp/...", "golang.org/x/tools/gopls/...") + }, "go/token", "golang.org/x/tools/gopls/internal/lsp/...", "golang.org/x/tools/gopls/...") if err != nil { t.Fatal(err) } @@ -30,7 +30,7 @@ func TestTokenOffset(t *testing.T) { switch pkg.PkgPath { case "go/token": tokenPkg = pkg - case "golang.org/x/tools/internal/lsp/safetoken": + case "golang.org/x/tools/gopls/internal/lsp/safetoken": safePkg = pkg } } @@ -39,7 +39,7 @@ func TestTokenOffset(t *testing.T) { t.Fatal("missing package go/token") } if safePkg == nil { - t.Fatal("missing package golang.org/x/tools/internal/lsp/safetoken") + t.Fatal("missing package golang.org/x/tools/gopls/internal/lsp/safetoken") } fileObj := tokenPkg.Types.Scope().Lookup("File") @@ -58,7 +58,7 @@ func TestTokenOffset(t *testing.T) { if safeOffset.Pos() <= ident.Pos() && ident.Pos() <= safeOffset.Scope().End() { continue // accepted usage } - t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/internal/lsp/safetoken.Offset instead.`, fset.Position(ident.Pos())) + t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/gopls/internal/lsp/safetoken.Offset instead.`, fset.Position(ident.Pos())) } } } diff --git a/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go similarity index 92% rename from internal/lsp/semantic.go rename to gopls/internal/lsp/semantic.go index 286d2fd160d..a8b38f044ab 100644 --- a/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -15,14 +15,15 @@ import ( "log" "path/filepath" "sort" + "strconv" "strings" "time" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" "golang.org/x/tools/internal/typeparams" ) @@ -107,14 +108,16 @@ func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocu return nil, err } e := &encoded{ - ctx: ctx, - pgf: pgf, - rng: rng, - ti: pkg.GetTypesInfo(), - pkg: pkg, - fset: snapshot.FileSet(), - tokTypes: s.session.Options().SemanticTypes, - tokMods: s.session.Options().SemanticMods, + ctx: ctx, + pgf: pgf, + rng: rng, + ti: pkg.GetTypesInfo(), + pkg: pkg, + fset: snapshot.FileSet(), + tokTypes: s.session.Options().SemanticTypes, + tokMods: s.session.Options().SemanticMods, + noStrings: vv.Options().NoSemanticString, + noNumbers: vv.Options().NoSemanticNumber, } if err := e.init(); err != nil { // e.init should never return an error, unless there's some @@ -186,7 +189,7 @@ func (e *encoded) token(start token.Pos, leng int, typ tokenType, mods []string) } // want a line and column from start (in LSP coordinates) // [//line directives should be ignored] - rng := source.NewMappedRange(e.fset, e.pgf.Mapper, start, start+token.Pos(leng)) + rng := source.NewMappedRange(e.pgf.Tok, e.pgf.Mapper, start, start+token.Pos(leng)) lspRange, err := rng.Range() if err != nil { // possibly a //line directive. TODO(pjw): fix this somehow @@ -223,6 +226,9 @@ type encoded struct { // the generated data items []semItem + noStrings bool + noNumbers bool + ctx context.Context tokTypes, tokMods []string pgf *source.ParsedGoFile @@ -299,11 +305,6 @@ func (e *encoded) inspector(n ast.Node) bool { what := tokNumber if x.Kind == token.STRING { what = tokString - if _, ok := e.stack[len(e.stack)-2].(*ast.Field); ok { - // struct tags (this is probably pointless, as the - // TextMate grammar will treat all the other comments the same) - what = tokComment - } } e.token(x.Pos(), ln, what, nil) case *ast.BinaryExpr: @@ -525,9 +526,14 @@ func (e *encoded) ident(x *ast.Ident) { tok(x.Pos(), len(x.Name), tokFunction, nil) } else if _, ok := y.Type().(*typeparams.TypeParam); ok { tok(x.Pos(), len(x.Name), tokTypeParam, nil) + } else if e.isParam(use.Pos()) { + // variable, unless use.pos is the pos of a Field in an ancestor FuncDecl + // or FuncLit and then it's a parameter + tok(x.Pos(), len(x.Name), tokParameter, nil) } else { tok(x.Pos(), len(x.Name), tokVariable, nil) } + default: // can't happen if use == nil { @@ -542,6 +548,30 @@ func (e *encoded) ident(x *ast.Ident) { } } +func (e *encoded) isParam(pos token.Pos) bool { + for i := len(e.stack) - 1; i >= 0; i-- { + switch n := e.stack[i].(type) { + case *ast.FuncDecl: + for _, f := range n.Type.Params.List { + for _, id := range f.Names { + if id.Pos() == pos { + return true + } + } + } + case *ast.FuncLit: + for _, f := range n.Type.Params.List { + for _, id := range f.Names { + if id.Pos() == pos { + return true + } + } + } + } + } + return false +} + func isSignature(use types.Object) bool { if true { return false //PJW: fix after generics seem ok @@ -639,7 +669,7 @@ func (e *encoded) unkIdent(x *ast.Ident) (tokenType, []string) { if nd.Tok != token.DEFINE { def = nil } - return tokVariable, def + return tokVariable, def // '_' in _ = ... } } // RHS, = x @@ -832,29 +862,36 @@ func (e *encoded) Data() []uint32 { var j int var last semItem for i := 0; i < len(e.items); i++ { - typ, ok := typeMap[e.items[i].typeStr] + item := e.items[i] + typ, ok := typeMap[item.typeStr] if !ok { continue // client doesn't want typeStr } + if item.typeStr == tokString && e.noStrings { + continue + } + if item.typeStr == tokNumber && e.noNumbers { + continue + } if j == 0 { x[0] = e.items[0].line } else { - x[j] = e.items[i].line - last.line + x[j] = item.line - last.line } - x[j+1] = e.items[i].start + x[j+1] = item.start if j > 0 && x[j] == 0 { - x[j+1] = e.items[i].start - last.start + x[j+1] = item.start - last.start } - x[j+2] = e.items[i].len + x[j+2] = item.len x[j+3] = uint32(typ) mask := 0 - for _, s := range e.items[i].mods { + for _, s := range item.mods { // modMap[s] is 0 if the client doesn't want this modifier mask |= modMap[s] } x[j+4] = uint32(mask) j += 5 - last = e.items[i] + last = item } return x[:j] } @@ -868,31 +905,25 @@ func (e *encoded) importSpec(d *ast.ImportSpec) { } return // don't mark anything for . or _ } - val := d.Path.Value - if len(val) < 2 || val[0] != '"' || val[len(val)-1] != '"' { - // avoid panics on imports without a properly quoted string + importPath, err := strconv.Unquote(d.Path.Value) + if err != nil { return } - nm := val[1 : len(val)-1] // remove surrounding "s // Import strings are implementation defined. Try to match with parse information. - x, err := e.pkg.GetImport(nm) + imported, err := e.pkg.ResolveImportPath(importPath) if err != nil { // unexpected, but impact is that maybe some import is not colored return } - // expect that nm is x.PkgPath and that x.Name() is a component of it - if x.PkgPath() != nm { - // don't know how or what to color (if this can happen at all) - return - } - // this is not a precise test: imagine "github.com/nasty/v/v2" - j := strings.LastIndex(nm, x.Name()) + // Check whether the original literal contains the package's declared name. + j := strings.LastIndex(d.Path.Value, imported.Name()) if j == -1 { // name doesn't show up, for whatever reason, so nothing to report return } - start := d.Path.Pos() + 1 + token.Pos(j) // skip the initial quote - e.token(start, len(x.Name()), tokNamespace, nil) + // Report virtual declaration at the position of the substring. + start := d.Path.Pos() + token.Pos(j) + e.token(start, len(imported.Name()), tokNamespace, nil) } // log unexpected state diff --git a/internal/lsp/server.go b/gopls/internal/lsp/server.go similarity index 91% rename from internal/lsp/server.go rename to gopls/internal/lsp/server.go index fb820cccfea..693afaedab5 100644 --- a/internal/lsp/server.go +++ b/gopls/internal/lsp/server.go @@ -10,18 +10,19 @@ import ( "fmt" "sync" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/progress" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/progress" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" ) const concurrentAnalyses = 1 // NewServer creates an LSP server and binds it to handle incoming client // messages on on the supplied stream. -func NewServer(session source.Session, client protocol.ClientCloser) *Server { +func NewServer(session *cache.Session, client protocol.ClientCloser) *Server { tracker := progress.NewTracker(client) session.SetProgressTracker(tracker) return &Server{ @@ -70,7 +71,7 @@ type Server struct { // notifications generated before serverInitialized notifications []*protocol.ShowMessageParams - session source.Session + session *cache.Session tempDir string @@ -123,7 +124,7 @@ type pendingModificationSet struct { changes []source.FileModification } -func (s *Server) workDoneProgressCancel(params *protocol.WorkDoneProgressCancelParams) error { +func (s *Server) workDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error { return s.progress.Cancel(params.Token) } diff --git a/internal/lsp/server_gen.go b/gopls/internal/lsp/server_gen.go similarity index 94% rename from internal/lsp/server_gen.go rename to gopls/internal/lsp/server_gen.go index 93b2f9913b8..8f4ab10a71f 100644 --- a/internal/lsp/server_gen.go +++ b/gopls/internal/lsp/server_gen.go @@ -9,7 +9,7 @@ package lsp import ( "context" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { @@ -20,10 +20,6 @@ func (s *Server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) return s.codeLens(ctx, params) } -func (s *Server) CodeLensRefresh(context.Context) error { - return notImplemented("CodeLensRefresh") -} - func (s *Server) ColorPresentation(context.Context, *protocol.ColorPresentationParams) ([]protocol.ColorPresentation, error) { return nil, notImplemented("ColorPresentation") } @@ -32,11 +28,11 @@ func (s *Server) Completion(ctx context.Context, params *protocol.CompletionPara return s.completion(ctx, params) } -func (s *Server) Declaration(context.Context, *protocol.DeclarationParams) (protocol.Declaration, error) { +func (s *Server) Declaration(context.Context, *protocol.DeclarationParams) (*protocol.Or_textDocument_declaration, error) { return nil, notImplemented("Declaration") } -func (s *Server) Definition(ctx context.Context, params *protocol.DefinitionParams) (protocol.Definition, error) { +func (s *Server) Definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) { return s.definition(ctx, params) } @@ -144,7 +140,7 @@ func (s *Server) Hover(ctx context.Context, params *protocol.HoverParams) (*prot return s.hover(ctx, params) } -func (s *Server) Implementation(ctx context.Context, params *protocol.ImplementationParams) (protocol.Definition, error) { +func (s *Server) Implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) { return s.implementation(ctx, params) } @@ -160,8 +156,8 @@ func (s *Server) Initialized(ctx context.Context, params *protocol.InitializedPa return s.initialized(ctx, params) } -func (s *Server) InlayHint(context.Context, *protocol.InlayHintParams) ([]protocol.InlayHint, error) { - return nil, notImplemented("InlayHint") +func (s *Server) InlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { + return s.inlayHint(ctx, params) } func (s *Server) InlayHintRefresh(context.Context) error { @@ -180,10 +176,6 @@ func (s *Server) LinkedEditingRange(context.Context, *protocol.LinkedEditingRang return nil, notImplemented("LinkedEditingRange") } -func (s *Server) LogTrace(context.Context, *protocol.LogTraceParams) error { - return notImplemented("LogTrace") -} - func (s *Server) Moniker(context.Context, *protocol.MonikerParams) ([]protocol.Moniker, error) { return nil, notImplemented("Moniker") } @@ -212,6 +204,10 @@ func (s *Server) PrepareTypeHierarchy(context.Context, *protocol.TypeHierarchyPr return nil, notImplemented("PrepareTypeHierarchy") } +func (s *Server) Progress(context.Context, *protocol.ProgressParams) error { + return notImplemented("Progress") +} + func (s *Server) RangeFormatting(context.Context, *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { return nil, notImplemented("RangeFormatting") } @@ -292,7 +288,7 @@ func (s *Server) Symbol(ctx context.Context, params *protocol.WorkspaceSymbolPar return s.symbol(ctx, params) } -func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) (protocol.Definition, error) { +func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) { return s.typeDefinition(ctx, params) } @@ -317,5 +313,5 @@ func (s *Server) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocume } func (s *Server) WorkDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error { - return s.workDoneProgressCancel(params) + return s.workDoneProgressCancel(ctx, params) } diff --git a/internal/lsp/signature_help.go b/gopls/internal/lsp/signature_help.go similarity index 85% rename from internal/lsp/signature_help.go rename to gopls/internal/lsp/signature_help.go index 24dee1b9a8d..8a343fbec81 100644 --- a/internal/lsp/signature_help.go +++ b/gopls/internal/lsp/signature_help.go @@ -8,9 +8,9 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) signatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) { diff --git a/internal/lsp/snippet/snippet_builder.go b/gopls/internal/lsp/snippet/snippet_builder.go similarity index 100% rename from internal/lsp/snippet/snippet_builder.go rename to gopls/internal/lsp/snippet/snippet_builder.go diff --git a/internal/lsp/snippet/snippet_builder_test.go b/gopls/internal/lsp/snippet/snippet_builder_test.go similarity index 100% rename from internal/lsp/snippet/snippet_builder_test.go rename to gopls/internal/lsp/snippet/snippet_builder_test.go diff --git a/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go similarity index 93% rename from internal/lsp/source/add_import.go rename to gopls/internal/lsp/source/add_import.go index 816acc2c25b..2fc03e5d758 100644 --- a/internal/lsp/source/add_import.go +++ b/gopls/internal/lsp/source/add_import.go @@ -8,7 +8,7 @@ import ( "context" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // AddImport adds a single import statement to the given file diff --git a/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go similarity index 82% rename from internal/lsp/source/api_json.go rename to gopls/internal/lsp/source/api_json.go index 0695efc2fa5..762054d10b8 100755 --- a/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -22,8 +22,8 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "directoryFilters", Type: "[]string", - Doc: "directoryFilters can be used to exclude unwanted directories from the\nworkspace. By default, all directories are included. Filters are an\noperator, `+` to include and `-` to exclude, followed by a path prefix\nrelative to the workspace folder. They are evaluated in order, and\nthe last filter that applies to a path controls whether it is included.\nThe path prefix can be empty, so an initial `-` excludes everything.\n\nExamples:\n\nExclude node_modules: `-node_modules`\n\nInclude only project_a: `-` (exclude everything), `+project_a`\n\nInclude only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`\n", - Default: "[\"-node_modules\"]", + Doc: "directoryFilters can be used to exclude unwanted directories from the\nworkspace. By default, all directories are included. Filters are an\noperator, `+` to include and `-` to exclude, followed by a path prefix\nrelative to the workspace folder. They are evaluated in order, and\nthe last filter that applies to a path controls whether it is included.\nThe path prefix can be empty, so an initial `-` excludes everything.\n\nDirectoryFilters also supports the `**` operator to match 0 or more directories.\n\nExamples:\n\nExclude node_modules at current depth: `-node_modules`\n\nExclude node_modules at any depth: `-**/node_modules`\n\nInclude only project_a: `-` (exclude everything), `+project_a`\n\nInclude only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`\n", + Default: "[\"-**/node_modules\"]", Hierarchy: "build", }, { @@ -59,7 +59,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "experimentalWorkspaceModule", Type: "bool", - Doc: "experimentalWorkspaceModule opts a user into the experimental support\nfor multi-module workspaces.\n", + Doc: "experimentalWorkspaceModule opts a user into the experimental support\nfor multi-module workspaces.\n\nDeprecated: this feature is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55331).\n", Default: "false", Status: "experimental", Hierarchy: "build", @@ -91,11 +91,18 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "experimentalUseInvalidMetadata", Type: "bool", - Doc: "experimentalUseInvalidMetadata enables gopls to fall back on outdated\npackage metadata to provide editor features if the go command fails to\nload packages for some reason (like an invalid go.mod file). This will\neventually be the default behavior, and this setting will be removed.\n", + Doc: "experimentalUseInvalidMetadata enables gopls to fall back on outdated\npackage metadata to provide editor features if the go command fails to\nload packages for some reason (like an invalid go.mod file).\n\nDeprecated: this setting is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55333).\n", Default: "false", Status: "experimental", Hierarchy: "build", }, + { + Name: "standaloneTags", + Type: "[]string", + Doc: "standaloneTags specifies a set of build constraints that identify\nindividual Go source files that make up the entire main package of an\nexecutable.\n\nA common example of standalone main files is the convention of using the\ndirective `//go:build ignore` to denote files that are not intended to be\nincluded in any package, for example because they are invoked directly by\nthe developer using `go run`.\n\nGopls considers a file to be a standalone main file if and only if it has\npackage name \"main\" and has a build directive of the exact form\n\"//go:build tag\" or \"// +build tag\", where tag is among the list of tags\nconfigured by this setting. Notably, if the build constraint is more\ncomplicated than a simple tag (such as the composite constraint\n`//go:build tag && go1.18`), the file is not considered to be a standalone\nmain file.\n\nThis setting is only supported when gopls is built with Go 1.16 or later.\n", + Default: "[\"ignore\"]", + Hierarchy: "build", + }, { Name: "hoverKind", Type: "enum", @@ -116,7 +123,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "linkTarget", Type: "string", - Doc: "linkTarget controls where documentation links go.\nIt might be one of:\n\n* `\"godoc.org\"`\n* `\"pkg.go.dev\"`\n\nIf company chooses to use its own `godoc.org`, its address can be used as well.\n", + Doc: "linkTarget controls where documentation links go.\nIt might be one of:\n\n* `\"godoc.org\"`\n* `\"pkg.go.dev\"`\n\nIf company chooses to use its own `godoc.org`, its address can be used as well.\n\nModules matching the GOPRIVATE environment variable will not have\ndocumentation links in hover.\n", Default: "\"pkg.go.dev\"", Hierarchy: "ui.documentation", }, @@ -214,7 +221,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "analyses", Type: "map[string]bool", - Doc: "analyses specify analyses that the user would like to enable or disable.\nA map of the names of analysis passes that should be enabled/disabled.\nA full list of analyzers that gopls uses can be found\n[here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).\n\nExample Usage:\n\n```json5\n...\n\"analyses\": {\n \"unreachable\": false, // Disable the unreachable analyzer.\n \"unusedparams\": true // Enable the unusedparams analyzer.\n}\n...\n```\n", + Doc: "analyses specify analyses that the user would like to enable or disable.\nA map of the names of analysis passes that should be enabled/disabled.\nA full list of analyzers that gopls uses can be found in\n[analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).\n\nExample Usage:\n\n```json5\n...\n\"analyses\": {\n \"unreachable\": false, // Disable the unreachable analyzer.\n \"unusedparams\": true // Enable the unusedparams analyzer.\n}\n...\n```\n", EnumKeys: EnumKeys{ ValueType: "bool", Keys: []EnumKey{ @@ -280,7 +287,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "\"fieldalignment\"", - Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the optimal order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n", + Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the most compact order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n\nBe aware that the most compact order is not always the most efficient.\nIn rare cases it may cause two variables each updated by its own goroutine\nto occupy the same CPU cache line, inducing a form of memory contention\nknown as \"false sharing\" that slows down both goroutines.\n", Default: "false", }, { @@ -300,7 +307,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "\"loopclosure\"", - Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a\nfunction literal inside the loop body. It checks only instances where\nthe function literal is called in a defer or go statement that is the\nlast statement in the loop body, as otherwise we would need whole\nprogram analysis.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", + Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n 3. a call testing.T.Run where the subtest body invokes t.Parallel()\n\nIn the case of (1) and (2), the analyzer only considers references in the last\nstatement of the loop body as it is not deep enough to understand the effects\nof subsequent statements which might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", Default: "true", }, { @@ -378,6 +385,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.", Default: "true", }, + { + Name: "\"timeformat\"", + Doc: "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.\n", + Default: "true", + }, { Name: "\"unmarshal\"", Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", @@ -433,6 +445,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "suggested fixes for \"undeclared name: <>\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will either insert a new statement,\nsuch as:\n\n\"<> := \"\n\nor a new function declaration, such as:\n\nfunc <>(inferred parameters) {\n\tpanic(\"implement me!\")\n}\n", Default: "true", }, + { + Name: "\"unusedvariable\"", + Doc: "check for unused variables\n\nThe unusedvariable analyzer suggests fixes for unused variables errors.\n", + Default: "false", + }, { Name: "\"fillstruct\"", Doc: "note incomplete struct initializations\n\nThis analyzer provides diagnostics for any struct literals that do not have\nany fields initialized. Because the suggested fix for this analysis is\nexpensive to compute, callers should compute it separately, using the\nSuggestedFix function below.\n", @@ -451,7 +468,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "staticcheck", Type: "bool", - Doc: "staticcheck enables additional analyses from staticcheck.io.\n", + Doc: "staticcheck enables additional analyses from staticcheck.io.\nThese analyses are documented on\n[Staticcheck's website](https://staticcheck.io/docs/checks/).\n", Default: "false", Status: "experimental", Hierarchy: "ui.diagnostic", @@ -500,11 +517,56 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "experimentalWatchedFileDelay", Type: "time.Duration", - Doc: "experimentalWatchedFileDelay controls the amount of time that gopls waits\nfor additional workspace/didChangeWatchedFiles notifications to arrive,\nbefore processing all such notifications in a single batch. This is\nintended for use by LSP clients that don't support their own batching of\nfile system notifications.\n\nThis option must be set to a valid duration string, for example `\"100ms\"`.\n", + Doc: "experimentalWatchedFileDelay controls the amount of time that gopls waits\nfor additional workspace/didChangeWatchedFiles notifications to arrive,\nbefore processing all such notifications in a single batch. This is\nintended for use by LSP clients that don't support their own batching of\nfile system notifications.\n\nThis option must be set to a valid duration string, for example `\"100ms\"`.\n\nDeprecated: this setting is deprecated and will be removed in a future\nversion of gopls (https://go.dev/issue/55332)\n", Default: "\"0s\"", Status: "experimental", Hierarchy: "ui.diagnostic", }, + { + Name: "hints", + Type: "map[string]bool", + Doc: "hints specify inlay hints that users want to see. A full list of hints\nthat gopls uses can be found in\n[inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n", + EnumKeys: EnumKeys{Keys: []EnumKey{ + { + Name: "\"assignVariableTypes\"", + Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", + Default: "false", + }, + { + Name: "\"compositeLiteralFields\"", + Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", + Default: "false", + }, + { + Name: "\"compositeLiteralTypes\"", + Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", + Default: "false", + }, + { + Name: "\"constantValues\"", + Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", + Default: "false", + }, + { + Name: "\"functionTypeParameters\"", + Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", + Default: "false", + }, + { + Name: "\"parameterNames\"", + Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", + Default: "false", + }, + { + Name: "\"rangeVariableTypes\"", + Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", + Default: "false", + }, + }}, + Default: "{}", + Status: "experimental", + Hierarchy: "ui.inlayhint", + }, { Name: "codelenses", Type: "map[string]bool", @@ -527,6 +589,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Regenerates cgo definitions.", Default: "true", }, + { + Name: "\"run_vulncheck_exp\"", + Doc: "Run vulnerability check (`govulncheck`).", + Default: "false", + }, { Name: "\"test\"", Doc: "Runs `go test` for a specific set of test or benchmark functions.", @@ -560,6 +627,22 @@ var GeneratedAPIJSON = &APIJSON{ Status: "experimental", Hierarchy: "ui", }, + { + Name: "noSemanticString", + Type: "bool", + Doc: "noSemanticString turns off the sending of the semantic token 'string'\n", + Default: "false", + Status: "experimental", + Hierarchy: "ui", + }, + { + Name: "noSemanticNumber", + Type: "bool", + Doc: "noSemanticNumber turns off the sending of the semantic token 'number'\n", + Default: "false", + Status: "experimental", + Hierarchy: "ui", + }, { Name: "local", Type: "string", @@ -664,6 +747,12 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Removes a dependency from the go.mod file of a module.", ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t\"OnlyDiagnostic\": bool,\n}", }, + { + Command: "gopls.reset_go_mod_diagnostics", + Title: "Reset go.mod diagnostics", + Doc: "Reset diagnostics in the go.mod file of a module.", + ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", + }, { Command: "gopls.run_tests", Title: "Run test(s)", @@ -671,11 +760,10 @@ var GeneratedAPIJSON = &APIJSON{ ArgDoc: "{\n\t// The test file containing the tests to run.\n\t\"URI\": string,\n\t// Specific test names to run, e.g. TestFoo.\n\t\"Tests\": []string,\n\t// Specific benchmarks to run, e.g. BenchmarkFoo.\n\t\"Benchmarks\": []string,\n}", }, { - Command: "gopls.run_vulncheck_exp", - Title: "Run vulncheck (experimental)", - Doc: "Run vulnerability check (`govulncheck`).", - ArgDoc: "{\n\t// Dir is the directory from which vulncheck will run from.\n\t\"Dir\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", - ResultDoc: "{\n\t\"Vuln\": []{\n\t\t\"ID\": string,\n\t\t\"Details\": string,\n\t\t\"Aliases\": []string,\n\t\t\"Symbol\": string,\n\t\t\"PkgPath\": string,\n\t\t\"ModPath\": string,\n\t\t\"URL\": string,\n\t\t\"CurrentVersion\": string,\n\t\t\"FixedVersion\": string,\n\t\t\"CallStacks\": [][]golang.org/x/tools/internal/lsp/command.StackEntry,\n\t\t\"CallStackSummaries\": []string,\n\t},\n}", + Command: "gopls.run_vulncheck_exp", + Title: "Run vulncheck (experimental)", + Doc: "Run vulnerability check (`govulncheck`).", + ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", }, { Command: "gopls.start_debugging", @@ -737,6 +825,11 @@ var GeneratedAPIJSON = &APIJSON{ Title: "Regenerate cgo", Doc: "Regenerates cgo definitions.", }, + { + Lens: "run_vulncheck_exp", + Title: "Run vulncheck (experimental)", + Doc: "Run vulnerability check (`govulncheck`).", + }, { Lens: "test", Title: "Run test(s) (legacy)", @@ -821,7 +914,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "fieldalignment", - Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the optimal order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n", + Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the most compact order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n\nBe aware that the most compact order is not always the most efficient.\nIn rare cases it may cause two variables each updated by its own goroutine\nto occupy the same CPU cache line, inducing a form of memory contention\nknown as \"false sharing\" that slows down both goroutines.\n", }, { Name: "httpresponse", @@ -840,7 +933,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Name: "loopclosure", - Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a\nfunction literal inside the loop body. It checks only instances where\nthe function literal is called in a defer or go statement that is the\nlast statement in the loop body, as otherwise we would need whole\nprogram analysis.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", + Doc: "check references to loop variables from within nested functions\n\nThis analyzer checks for references to loop variables from within a function\nliteral inside the loop body. It checks for patterns where access to a loop\nvariable is known to escape the current loop iteration:\n 1. a call to go or defer at the end of the loop body\n 2. a call to golang.org/x/sync/errgroup.Group.Go at the end of the loop body\n 3. a call testing.T.Run where the subtest body invokes t.Parallel()\n\nIn the case of (1) and (2), the analyzer only considers references in the last\nstatement of the loop body as it is not deep enough to understand the effects\nof subsequent statements which might render the reference benign.\n\nFor example:\n\n\tfor i, v := range s {\n\t\tgo func() {\n\t\t\tprintln(i, v) // not what you might expect\n\t\t}()\n\t}\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", Default: true, }, { @@ -916,6 +1009,11 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.", Default: true, }, + { + Name: "timeformat", + Doc: "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.\n", + Default: true, + }, { Name: "unmarshal", Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", @@ -968,6 +1066,10 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "suggested fixes for \"undeclared name: <>\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will either insert a new statement,\nsuch as:\n\n\"<> := \"\n\nor a new function declaration, such as:\n\nfunc <>(inferred parameters) {\n\tpanic(\"implement me!\")\n}\n", Default: true, }, + { + Name: "unusedvariable", + Doc: "check for unused variables\n\nThe unusedvariable analyzer suggests fixes for unused variables errors.\n", + }, { Name: "fillstruct", Doc: "note incomplete struct initializations\n\nThis analyzer provides diagnostics for any struct literals that do not have\nany fields initialized. Because the suggested fix for this analysis is\nexpensive to compute, callers should compute it separately, using the\nSuggestedFix function below.\n", @@ -979,4 +1081,34 @@ var GeneratedAPIJSON = &APIJSON{ Default: true, }, }, + Hints: []*HintJSON{ + { + Name: "assignVariableTypes", + Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", + }, + { + Name: "compositeLiteralFields", + Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", + }, + { + Name: "compositeLiteralTypes", + Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", + }, + { + Name: "constantValues", + Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", + }, + { + Name: "functionTypeParameters", + Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", + }, + { + Name: "parameterNames", + Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", + }, + { + Name: "rangeVariableTypes", + Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", + }, + }, } diff --git a/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go similarity index 91% rename from internal/lsp/source/call_hierarchy.go rename to gopls/internal/lsp/source/call_hierarchy.go index c2c8a1866d0..1097d629072 100644 --- a/internal/lsp/source/call_hierarchy.go +++ b/gopls/internal/lsp/source/call_hierarchy.go @@ -14,10 +14,10 @@ import ( "path/filepath" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/event/tag" ) // PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file. @@ -152,12 +152,12 @@ outer: kind = protocol.Function } - nameStart, nameEnd := nameIdent.NamePos, nameIdent.NamePos+token.Pos(len(nameIdent.Name)) + nameStart, nameEnd := nameIdent.Pos(), nameIdent.End() if funcLit != nil { nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos() kind = protocol.Function } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, nameStart, nameEnd).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, nameStart, nameEnd).Range() if err != nil { return protocol.CallHierarchyItem{}, err } @@ -194,14 +194,22 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr if _, ok := identifier.Declaration.obj.Type().Underlying().(*types.Signature); !ok { return nil, nil } - if identifier.Declaration.node == nil { + node := identifier.Declaration.node + if node == nil { return nil, nil } if len(identifier.Declaration.MappedRange) == 0 { return nil, nil } declMappedRange := identifier.Declaration.MappedRange[0] - callExprs, err := collectCallExpressions(snapshot.FileSet(), declMappedRange.m, identifier.Declaration.node) + // TODO(adonovan): avoid Fileset.File call by somehow getting at + // declMappedRange.spanRange.TokFile, or making Identifier retain the + // token.File of the identifier and its declaration, since it looks up both anyway. + tokFile := snapshot.FileSet().File(node.Pos()) + if tokFile == nil { + return nil, fmt.Errorf("no file for position") + } + callExprs, err := collectCallExpressions(tokFile, declMappedRange.m, node) if err != nil { return nil, err } @@ -210,7 +218,7 @@ func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr } // collectCallExpressions collects call expression ranges inside a function. -func collectCallExpressions(fset *token.FileSet, mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) { +func collectCallExpressions(tokFile *token.File, mapper *protocol.ColumnMapper, node ast.Node) ([]protocol.Range, error) { type callPos struct { start, end token.Pos } @@ -240,7 +248,7 @@ func collectCallExpressions(fset *token.FileSet, mapper *protocol.ColumnMapper, callRanges := []protocol.Range{} for _, call := range callPositions { - callRange, err := NewMappedRange(fset, mapper, call.start, call.end).Range() + callRange, err := NewMappedRange(tokFile, mapper, call.start, call.end).Range() if err != nil { return nil, err } diff --git a/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go similarity index 89% rename from internal/lsp/source/code_lens.go rename to gopls/internal/lsp/source/code_lens.go index 0ab857ac600..83ffcc28d29 100644 --- a/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -13,9 +13,9 @@ import ( "regexp" "strings" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" ) type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error) @@ -67,7 +67,7 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p return nil, err } // add a code lens to the top of the file which runs all benchmarks in the file - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err } @@ -111,7 +111,7 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) ( continue } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, d.Pos(), fn.End()).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, fn.Pos(), fn.End()).Range() if err != nil { return out, err } @@ -177,7 +177,7 @@ func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ( if !strings.HasPrefix(l.Text, ggDirective) { continue } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() if err != nil { return nil, err } @@ -214,7 +214,7 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ if c == nil { return nil, nil } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, c.Pos(), c.EndPos).Range() + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, c.Pos(), c.End()).Range() if err != nil { return nil, err } @@ -231,7 +231,11 @@ func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle if err != nil { return nil, err } - rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + if !pgf.File.Package.IsValid() { + // Without a package name we have nowhere to put the codelens, so give up. + return nil, nil + } + rng, err := NewMappedRange(pgf.Tok, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() if err != nil { return nil, err } diff --git a/internal/lsp/source/comment.go b/gopls/internal/lsp/source/comment.go similarity index 99% rename from internal/lsp/source/comment.go rename to gopls/internal/lsp/source/comment.go index 000d6136c80..ff6d11f4ff7 100644 --- a/internal/lsp/source/comment.go +++ b/gopls/internal/lsp/source/comment.go @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !go1.19 +// +build !go1.19 + package source import ( @@ -226,7 +229,7 @@ func unindent(block []string) { prefix := block[0][0:indentLen(block[0])] for _, line := range block { if !isBlank(line) { - prefix = commonPrefix(prefix, line[0:indentLen(line)]) + prefix = commonPrefix(prefix, line) } } n := len(prefix) diff --git a/gopls/internal/lsp/source/comment_go118.go b/gopls/internal/lsp/source/comment_go118.go new file mode 100644 index 00000000000..0503670d3bf --- /dev/null +++ b/gopls/internal/lsp/source/comment_go118.go @@ -0,0 +1,37 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package source + +// Starting with go1.19, the formatting of comments has changed, and there +// is a new package (go/doc/comment) for processing them. +// As long as gopls has to compile under earlier versions, tests +// have to pass with both the old and new code, which produce +// slightly different results. (cmd/test/definition.go, source/comment_test.go, +// and source/source_test.go) Each of the test files checks the results +// with a function, tests.CheckSameMarkdown, that accepts both the old and the new +// results. (The old code escapes many characters the new code does not, +// and the new code sometimes adds a blank line.) + +// When gopls no longer needs to compile with go1.18, the old comment.go should +// be replaced by this file, the golden test files should be updated. +// (and checkSameMarkdown() could be replaced by a simple comparison.) + +import "go/doc/comment" + +// CommentToMarkdown converts comment text to formatted markdown. +// The comment was prepared by DocReader, +// so it is known not to have leading, trailing blank lines +// nor to have trailing spaces at the end of lines. +// The comment markers have already been removed. +func CommentToMarkdown(text string) string { + var p comment.Parser + doc := p.Parse(text) + var pr comment.Printer + easy := pr.Markdown(doc) + return string(easy) +} diff --git a/internal/lsp/source/comment_test.go b/gopls/internal/lsp/source/comment_go118_test.go similarity index 99% rename from internal/lsp/source/comment_test.go rename to gopls/internal/lsp/source/comment_go118_test.go index 9efde16ef3c..b48b2e753ce 100644 --- a/internal/lsp/source/comment_test.go +++ b/gopls/internal/lsp/source/comment_go118_test.go @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !go1.19 +// +build !go1.19 + package source import ( diff --git a/internal/lsp/source/completion/builtin.go b/gopls/internal/lsp/source/completion/builtin.go similarity index 100% rename from internal/lsp/source/completion/builtin.go rename to gopls/internal/lsp/source/completion/builtin.go diff --git a/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go similarity index 98% rename from internal/lsp/source/completion/completion.go rename to gopls/internal/lsp/source/completion/completion.go index bb1c68d2238..c3b7c2b461b 100644 --- a/internal/lsp/source/completion/completion.go +++ b/gopls/internal/lsp/source/completion/completion.go @@ -23,13 +23,13 @@ import ( "unicode" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/fuzzy" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) @@ -173,8 +173,9 @@ type completer struct { // file is the AST of the file associated with this completion request. file *ast.File - // pos is the position at which the request was triggered. - pos token.Pos + // (tokFile, pos) is the position at which the request was triggered. + tokFile *token.File + pos token.Pos // path is the path of AST nodes enclosing the position. path []ast.Node @@ -263,7 +264,6 @@ type compLitInfo struct { type importInfo struct { importPath string name string - pkg source.Package } type methodSetKey struct { @@ -325,7 +325,7 @@ func (c *completer) setSurrounding(ident *ast.Ident) { content: ident.Name, cursor: c.pos, // Overwrite the prefix only. - rng: span.NewRange(c.snapshot.FileSet(), ident.Pos(), ident.End()), + rng: span.NewRange(c.tokFile, ident.Pos(), ident.End()), } c.setMatcherFromPrefix(c.surrounding.Prefix()) @@ -347,7 +347,7 @@ func (c *completer) getSurrounding() *Selection { c.surrounding = &Selection{ content: "", cursor: c.pos, - rng: span.NewRange(c.snapshot.FileSet(), c.pos, c.pos), + rng: span.NewRange(c.tokFile, c.pos, c.pos), } } return c.surrounding @@ -441,7 +441,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan items, surrounding, innerErr := packageClauseCompletions(ctx, snapshot, fh, protoPos) if innerErr != nil { // return the error for GetParsedFile since it's more relevant in this situation. - return nil, nil, fmt.Errorf("getting file for Completion: %w (package completions: %v)", err, innerErr) + return nil, nil, fmt.Errorf("getting file %s for Completion: %w (package completions: %v)", fh.URI(), err, innerErr) } return items, surrounding, nil } @@ -486,7 +486,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan qual := types.RelativeTo(pkg.GetTypes()) objStr = types.ObjectString(obj, qual) } - ans, sel := definition(path, obj, snapshot.FileSet(), fh) + ans, sel := definition(path, obj, pgf.Tok, fh) if ans != nil { sort.Slice(ans, func(i, j int) bool { return ans[i].Score > ans[j].Score @@ -513,6 +513,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan }, fh: fh, filename: fh.URI().Filename(), + tokFile: pgf.Tok, file: pgf.File, path: path, pos: pos, @@ -798,7 +799,7 @@ func (c *completer) populateImportCompletions(ctx context.Context, searchImport c.surrounding = &Selection{ content: content, cursor: c.pos, - rng: span.NewRange(c.snapshot.FileSet(), start, end), + rng: span.NewRange(c.tokFile, start, end), } seenImports := make(map[string]struct{}) @@ -1018,7 +1019,7 @@ func (c *completer) setSurroundingForComment(comments *ast.CommentGroup) { c.surrounding = &Selection{ content: cursorComment.Text[start:end], cursor: c.pos, - rng: span.NewRange(c.snapshot.FileSet(), token.Pos(int(cursorComment.Slash)+start), token.Pos(int(cursorComment.Slash)+end)), + rng: span.NewRange(c.tokFile, token.Pos(int(cursorComment.Slash)+start), token.Pos(int(cursorComment.Slash)+end)), } c.setMatcherFromPrefix(c.surrounding.Prefix()) } @@ -1163,7 +1164,6 @@ func (c *completer) unimportedMembers(ctx context.Context, id *ast.Ident) error } imp := &importInfo{ importPath: path, - pkg: pkg, } if imports.ImportPathToAssumedName(path) != pkg.GetTypes().Name() { imp.name = pkg.GetTypes().Name() @@ -1243,7 +1243,7 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *impo c.methodSetCache[methodSetKey{typ, addressable}] = mset } - if typ.String() == "*testing.F" && addressable { + if isStarTestingDotF(typ) && addressable { // is that a sufficient test? (or is more care needed?) if c.fuzz(typ, mset, imp, cb, c.snapshot.FileSet()) { return @@ -1270,6 +1270,21 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *impo }) } +// isStarTestingDotF reports whether typ is *testing.F. +func isStarTestingDotF(typ types.Type) bool { + ptr, _ := typ.(*types.Pointer) + if ptr == nil { + return false + } + named, _ := ptr.Elem().(*types.Named) + if named == nil { + return false + } + obj := named.Obj() + // obj.Pkg is nil for the error type. + return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == "F" +} + // lexical finds completions in the lexical environment. func (c *completer) lexical(ctx context.Context) error { var ( @@ -1502,7 +1517,6 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru } imp := &importInfo{ importPath: path, - pkg: pkg, } if imports.ImportPathToAssumedName(path) != pkg.GetTypes().Name() { imp.name = pkg.GetTypes().Name() @@ -2314,7 +2328,7 @@ func (ci candidateInference) applyTypeNameModifiers(typ types.Type) types.Type { // matchesVariadic returns true if we are completing a variadic // parameter and candType is a compatible slice type. func (ci candidateInference) matchesVariadic(candType types.Type) bool { - return ci.variadic && ci.objType != nil && types.AssignableTo(candType, types.NewSlice(ci.objType)) + return ci.variadic && ci.objType != nil && assignableTo(candType, types.NewSlice(ci.objType)) } // findSwitchStmt returns an *ast.CaseClause's corresponding *ast.SwitchStmt or @@ -2640,7 +2654,7 @@ func (ci *candidateInference) candTypeMatches(cand *candidate) bool { return false } - if ci.convertibleTo != nil && types.ConvertibleTo(candType, ci.convertibleTo) { + if ci.convertibleTo != nil && convertibleTo(candType, ci.convertibleTo) { return true } @@ -2728,7 +2742,7 @@ func considerTypeConversion(from, to types.Type, path []types.Object) bool { return false } - if !types.ConvertibleTo(from, to) { + if !convertibleTo(from, to) { return false } @@ -2777,7 +2791,7 @@ func (ci *candidateInference) typeMatches(expType, candType types.Type) bool { // AssignableTo covers the case where the types are equal, but also handles // cases like assigning a concrete type to an interface type. - return types.AssignableTo(candType, expType) + return assignableTo(candType, expType) } // kindMatches reports whether candType's kind matches our expected @@ -2840,7 +2854,7 @@ func (ci *candidateInference) assigneesMatch(cand *candidate, sig *types.Signatu assignee = ci.assignees[i] } - if assignee == nil { + if assignee == nil || assignee == types.Typ[types.Invalid] { continue } @@ -2894,7 +2908,7 @@ func (c *completer) matchingTypeName(cand *candidate) bool { // // Where our expected type is "[]int", and we expect a type name. if c.inference.objType != nil { - return types.AssignableTo(candType, c.inference.objType) + return assignableTo(candType, c.inference.objType) } // Default to saying any type name is a match. diff --git a/internal/lsp/source/completion/deep_completion.go b/gopls/internal/lsp/source/completion/deep_completion.go similarity index 100% rename from internal/lsp/source/completion/deep_completion.go rename to gopls/internal/lsp/source/completion/deep_completion.go diff --git a/internal/lsp/source/completion/deep_completion_test.go b/gopls/internal/lsp/source/completion/deep_completion_test.go similarity index 100% rename from internal/lsp/source/completion/deep_completion_test.go rename to gopls/internal/lsp/source/completion/deep_completion_test.go diff --git a/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go similarity index 90% rename from internal/lsp/source/completion/definition.go rename to gopls/internal/lsp/source/completion/definition.go index 44d5a33b2f4..fe41c55b3b8 100644 --- a/internal/lsp/source/completion/definition.go +++ b/gopls/internal/lsp/source/completion/definition.go @@ -12,10 +12,10 @@ import ( "unicode" "unicode/utf8" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) // some definitions can be completed @@ -23,7 +23,7 @@ import ( // BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F) // path[0] is known to be *ast.Ident -func definition(path []ast.Node, obj types.Object, fset *token.FileSet, fh source.FileHandle) ([]CompletionItem, *Selection) { +func definition(path []ast.Node, obj types.Object, tokFile *token.File, fh source.FileHandle) ([]CompletionItem, *Selection) { if _, ok := obj.(*types.Func); !ok { return nil, nil // not a function at all } @@ -40,7 +40,7 @@ func definition(path []ast.Node, obj types.Object, fset *token.FileSet, fh sourc sel := &Selection{ content: "", cursor: pos, - rng: span.NewRange(fset, pos, pos), + rng: span.NewRange(tokFile, pos, pos), } var ans []CompletionItem diff --git a/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go similarity index 95% rename from internal/lsp/source/completion/format.go rename to gopls/internal/lsp/source/completion/format.go index 72498cc6874..c2693dc12d4 100644 --- a/internal/lsp/source/completion/format.go +++ b/gopls/internal/lsp/source/completion/format.go @@ -13,13 +13,13 @@ import ( "go/types" "strings" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) @@ -80,7 +80,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e if _, ok := obj.Type().(*types.Struct); ok { detail = "struct{...}" // for anonymous structs } else if obj.IsField() { - detail = source.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf) + detail = source.FormatVarType(c.snapshot.FileSet(), c.pkg, obj, c.qf) } if obj.IsField() { kind = protocol.FieldCompletion @@ -237,15 +237,12 @@ Suffixes: uri := span.URIFromPath(pos.Filename) // Find the source file of the candidate. - pkg, err := source.FindPackageFromPos(ctx, c.snapshot, obj.Pos()) + pkg, err := source.FindPackageFromPos(c.snapshot.FileSet(), c.pkg, obj.Pos()) if err != nil { return item, nil } - decl, err := c.snapshot.PosToDecl(ctx, pkg, obj.Pos()) - if err != nil { - return CompletionItem{}, err - } + decl, _ := source.FindDeclAndField(pkg.GetSyntax(), obj.Pos()) // may be nil hover, err := source.FindHoverContext(ctx, c.snapshot, pkg, obj, decl, nil) if err != nil { event.Error(ctx, "failed to find Hover", err, tag.URI.Of(uri)) diff --git a/internal/lsp/source/completion/fuzz.go b/gopls/internal/lsp/source/completion/fuzz.go similarity index 98% rename from internal/lsp/source/completion/fuzz.go rename to gopls/internal/lsp/source/completion/fuzz.go index 92349ab9343..d7912ceabc6 100644 --- a/internal/lsp/source/completion/fuzz.go +++ b/gopls/internal/lsp/source/completion/fuzz.go @@ -11,7 +11,7 @@ import ( "go/types" "strings" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // golang/go#51089 diff --git a/internal/lsp/source/completion/keywords.go b/gopls/internal/lsp/source/completion/keywords.go similarity index 97% rename from internal/lsp/source/completion/keywords.go rename to gopls/internal/lsp/source/completion/keywords.go index bbf59b0221f..a068ca2d57c 100644 --- a/internal/lsp/source/completion/keywords.go +++ b/gopls/internal/lsp/source/completion/keywords.go @@ -7,8 +7,8 @@ package completion import ( "go/ast" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) const ( diff --git a/internal/lsp/source/completion/labels.go b/gopls/internal/lsp/source/completion/labels.go similarity index 100% rename from internal/lsp/source/completion/labels.go rename to gopls/internal/lsp/source/completion/labels.go diff --git a/internal/lsp/source/completion/literal.go b/gopls/internal/lsp/source/completion/literal.go similarity index 96% rename from internal/lsp/source/completion/literal.go rename to gopls/internal/lsp/source/completion/literal.go index 139ec17dc05..0a9fc83e66f 100644 --- a/internal/lsp/source/completion/literal.go +++ b/gopls/internal/lsp/source/completion/literal.go @@ -11,10 +11,10 @@ import ( "strings" "unicode" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/typeparams" ) @@ -162,7 +162,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im if score := c.matcher.Score("func"); !cand.hasMod(reference) && score > 0 && !source.IsInterface(expType) { switch t := literalType.Underlying().(type) { case *types.Signature: - c.functionLiteral(ctx, t, float64(score)) + c.functionLiteral(t, float64(score)) } } } @@ -175,7 +175,7 @@ const literalCandidateScore = highScore / 2 // functionLiteral adds a function literal completion item for the // given signature. -func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, matchScore float64) { +func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) { snip := &snippet.Builder{} snip.WriteText("func(") @@ -202,7 +202,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m // If the param has no name in the signature, guess a name based // on the type. Use an empty qualifier to ignore the package. // For example, we want to name "http.Request" "r", not "hr". - name = source.FormatVarType(ctx, c.snapshot, c.pkg, p, func(p *types.Package) string { + name = source.FormatVarType(c.snapshot.FileSet(), c.pkg, p, func(p *types.Package) string { return "" }) name = abbreviateTypeName(name) @@ -264,7 +264,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 := source.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf) + typeStr := source.FormatVarType(c.snapshot.FileSet(), c.pkg, p, c.qf) if sig.Variadic() && i == sig.Params().Len()-1 { typeStr = strings.Replace(typeStr, "[]", "...", 1) } @@ -314,7 +314,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m snip.WriteText(name + " ") } - text := source.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf) + text := source.FormatVarType(c.snapshot.FileSet(), c.pkg, r, c.qf) if tp, _ := r.Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { snip.WritePlaceholder(func(snip *snippet.Builder) { snip.WriteText(text) diff --git a/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go similarity index 94% rename from internal/lsp/source/completion/package.go rename to gopls/internal/lsp/source/completion/package.go index 21244efb5ec..b7fad0fa513 100644 --- a/internal/lsp/source/completion/package.go +++ b/gopls/internal/lsp/source/completion/package.go @@ -18,12 +18,12 @@ import ( "strings" "unicode" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/fuzzy" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/fuzzy" ) // packageClauseCompletions offers completions for a package declaration when @@ -104,7 +104,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, return &Selection{ content: name.Name, cursor: cursor, - rng: span.NewRange(fset, name.Pos(), name.End()), + rng: span.NewRange(tok, name.Pos(), name.End()), }, nil } } @@ -141,7 +141,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, return &Selection{ content: content, cursor: cursor, - rng: span.NewRange(fset, start, end), + rng: span.NewRange(tok, start, end), }, nil } } @@ -154,7 +154,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, } // If the cursor is in a comment, don't offer any completions. - if cursorInComment(fset, cursor, pgf.Src) { + if cursorInComment(fset.File(cursor), cursor, pgf.Src) { return nil, fmt.Errorf("cursor in comment") } @@ -168,13 +168,13 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile, return &Selection{ content: "", cursor: cursor, - rng: span.NewRange(fset, start, end), + rng: span.NewRange(tok, start, end), }, nil } -func cursorInComment(fset *token.FileSet, cursor token.Pos, src []byte) bool { +func cursorInComment(file *token.File, cursor token.Pos, src []byte) bool { var s scanner.Scanner - s.Init(fset.File(cursor), src, func(_ token.Position, _ string) {}, scanner.ScanComments) + s.Init(file, src, func(_ token.Position, _ string) {}, scanner.ScanComments) for { pos, tok, lit := s.Scan() if pos <= cursor && cursor <= token.Pos(int(pos)+len(lit)) { diff --git a/internal/lsp/source/completion/package_test.go b/gopls/internal/lsp/source/completion/package_test.go similarity index 100% rename from internal/lsp/source/completion/package_test.go rename to gopls/internal/lsp/source/completion/package_test.go diff --git a/internal/lsp/source/completion/postfix_snippets.go b/gopls/internal/lsp/source/completion/postfix_snippets.go similarity index 97% rename from internal/lsp/source/completion/postfix_snippets.go rename to gopls/internal/lsp/source/completion/postfix_snippets.go index d7f0d90da9e..414db42592e 100644 --- a/internal/lsp/source/completion/postfix_snippets.go +++ b/gopls/internal/lsp/source/completion/postfix_snippets.go @@ -18,9 +18,9 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" ) // Postfix snippets are artificial methods that allow the user to @@ -149,6 +149,14 @@ for {{.VarName .KeyType "k"}}, {{.VarName .ElemType "v"}} := range {{.X}} { {{$keysVar}} = append({{$keysVar}}, {{$k}}) } {{end}}`, +}, { + label: "range", + details: "range over channel", + body: `{{if and (eq .Kind "chan") .StmtOK -}} +for {{.VarName .ElemType "e"}} := range {{.X}} { + {{.Cursor}} +} +{{- end}}`, }, { label: "var", details: "assign to variables", diff --git a/internal/lsp/source/completion/printf.go b/gopls/internal/lsp/source/completion/printf.go similarity index 100% rename from internal/lsp/source/completion/printf.go rename to gopls/internal/lsp/source/completion/printf.go diff --git a/internal/lsp/source/completion/printf_test.go b/gopls/internal/lsp/source/completion/printf_test.go similarity index 100% rename from internal/lsp/source/completion/printf_test.go rename to gopls/internal/lsp/source/completion/printf_test.go diff --git a/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go similarity index 98% rename from internal/lsp/source/completion/snippet.go rename to gopls/internal/lsp/source/completion/snippet.go index 72c351f946e..1a9ebb1d4be 100644 --- a/internal/lsp/source/completion/snippet.go +++ b/gopls/internal/lsp/source/completion/snippet.go @@ -7,7 +7,7 @@ package completion import ( "go/ast" - "golang.org/x/tools/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/snippet" ) // structFieldSnippets calculates the snippet for struct literal field names. diff --git a/internal/lsp/source/completion/statements.go b/gopls/internal/lsp/source/completion/statements.go similarity index 97% rename from internal/lsp/source/completion/statements.go rename to gopls/internal/lsp/source/completion/statements.go index d8e30a2d5b2..1f80193f194 100644 --- a/internal/lsp/source/completion/statements.go +++ b/gopls/internal/lsp/source/completion/statements.go @@ -10,9 +10,9 @@ import ( "go/token" "go/types" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/snippet" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" ) // addStatementCandidates adds full statement completion candidates @@ -336,7 +336,7 @@ func getTestVar(enclosingFunc *funcInfo, pkg source.Package) string { if param.Name() == "_" { continue } - testingPkg, err := pkg.GetImport("testing") + testingPkg, err := pkg.DirectDep("testing") if err != nil { continue } diff --git a/internal/lsp/source/completion/util.go b/gopls/internal/lsp/source/completion/util.go similarity index 88% rename from internal/lsp/source/completion/util.go rename to gopls/internal/lsp/source/completion/util.go index cd7849af262..72877a38a35 100644 --- a/internal/lsp/source/completion/util.go +++ b/gopls/internal/lsp/source/completion/util.go @@ -10,9 +10,10 @@ import ( "go/types" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/typeparams" ) @@ -311,13 +312,37 @@ func isBasicKind(t types.Type, k types.BasicInfo) bool { } func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.TextEdit, error) { - rng := source.NewMappedRange(c.snapshot.FileSet(), c.mapper, from, to) - spn, err := rng.Span() + start, err := safetoken.Offset(c.tokFile, from) if err != nil { - return nil, err + return nil, err // can't happen: from came from c } - return source.ToProtocolEdits(c.mapper, []diff.TextEdit{{ - Span: spn, - NewText: newText, + end, err := safetoken.Offset(c.tokFile, to) + if err != nil { + return nil, err // can't happen: to came from c + } + return source.ToProtocolEdits(c.mapper, []diff.Edit{{ + Start: start, + End: end, + New: newText, }}) } + +// assignableTo is like types.AssignableTo, but returns false if +// either type is invalid. +func assignableTo(x, to types.Type) bool { + if x == types.Typ[types.Invalid] || to == types.Typ[types.Invalid] { + return false + } + + return types.AssignableTo(x, to) +} + +// convertibleTo is like types.ConvertibleTo, but returns false if +// either type is invalid. +func convertibleTo(x, to types.Type) bool { + if x == types.Typ[types.Invalid] || to == types.Typ[types.Invalid] { + return false + } + + return types.ConvertibleTo(x, to) +} diff --git a/internal/lsp/source/completion/util_test.go b/gopls/internal/lsp/source/completion/util_test.go similarity index 100% rename from internal/lsp/source/completion/util_test.go rename to gopls/internal/lsp/source/completion/util_test.go diff --git a/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go similarity index 96% rename from internal/lsp/source/diagnostics.go rename to gopls/internal/lsp/source/diagnostics.go index e393c2f9426..c292f257989 100644 --- a/internal/lsp/source/diagnostics.go +++ b/gopls/internal/lsp/source/diagnostics.go @@ -7,8 +7,8 @@ package source import ( "context" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" ) type SuggestedFix struct { diff --git a/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go similarity index 92% rename from internal/lsp/source/extract.go rename to gopls/internal/lsp/source/extract.go index 90999d821a6..7f29d45ba2f 100644 --- a/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -12,17 +12,20 @@ import ( "go/parser" "go/token" "go/types" + "sort" "strings" - "unicode" + "text/scanner" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/bug" ) func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { + tokFile := fset.File(file.Pos()) expr, path, ok, err := CanExtractVariable(rng, file) if !ok { return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", fset.Position(rng.Start), err) @@ -60,11 +63,7 @@ func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast. if insertBeforeStmt == nil { return nil, fmt.Errorf("cannot find location to insert extraction") } - tok := fset.File(expr.Pos()) - if tok == nil { - return nil, fmt.Errorf("no file for pos %v", fset.Position(file.Pos())) - } - indent, err := calculateIndentation(src, tok, insertBeforeStmt) + indent, err := calculateIndentation(src, tokFile, insertBeforeStmt) if err != nil { return nil, err } @@ -217,7 +216,12 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file if isMethod { errorPrefix = "extractMethod" } - p, ok, methodOk, err := CanExtractFunction(fset, rng, src, file) + + tok := fset.File(file.Pos()) + if tok == nil { + return nil, bug.Errorf("no file for position") + } + p, ok, methodOk, err := CanExtractFunction(tok, rng, src, file) if (!ok && !isMethod) || (!methodOk && isMethod) { return nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, fset.Position(rng.Start), err) @@ -344,7 +348,7 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file if v.obj.Parent() == nil { return nil, fmt.Errorf("parent nil") } - isUsed, firstUseAfter := objUsed(info, span.NewRange(fset, rng.End, v.obj.Parent().End()), v.obj) + isUsed, firstUseAfter := objUsed(info, span.NewRange(tok, rng.End, v.obj.Parent().End()), v.obj) if v.assigned && isUsed && !varOverridden(info, firstUseAfter, v.obj, v.free, outer) { returnTypes = append(returnTypes, &ast.Field{Type: typ}) returns = append(returns, identifier) @@ -647,47 +651,83 @@ func extractFunctionMethod(fset *token.FileSet, rng span.Range, src []byte, file }, nil } -// adjustRangeForWhitespace adjusts the given range to exclude unnecessary leading or -// trailing whitespace characters from selection. In the following example, each line -// of the if statement is indented once. There are also two extra spaces after the -// closing bracket before the line break. +// adjustRangeForCommentsAndWhiteSpace adjusts the given range to exclude unnecessary leading or +// trailing whitespace characters from selection as well as leading or trailing comments. +// In the following example, each line of the if statement is indented once. There are also two +// extra spaces after the sclosing bracket before the line break and a comment. // // \tif (true) { // \t _ = 1 -// \t} \n +// \t} // hello \n // // By default, a valid range begins at 'if' and ends at the first whitespace character // after the '}'. But, users are likely to highlight full lines rather than adjusting // their cursors for whitespace. To support this use case, we must manually adjust the // ranges to match the correct AST node. In this particular example, we would adjust -// rng.Start forward by one byte, and rng.End backwards by two bytes. -func adjustRangeForWhitespace(rng span.Range, tok *token.File, content []byte) (span.Range, error) { - offset, err := safetoken.Offset(tok, rng.Start) - if err != nil { - return span.Range{}, err - } - for offset < len(content) { - if !unicode.IsSpace(rune(content[offset])) { - break +// rng.Start forward to the start of 'if' and rng.End backward to after '}'. +func adjustRangeForCommentsAndWhiteSpace(rng span.Range, tok *token.File, content []byte, file *ast.File) (span.Range, error) { + // Adjust the end of the range to after leading whitespace and comments. + prevStart, start := token.NoPos, rng.Start + startComment := sort.Search(len(file.Comments), func(i int) bool { + // Find the index for the first comment that ends after range start. + return file.Comments[i].End() > rng.Start + }) + for prevStart != start { + prevStart = start + // If start is within a comment, move start to the end + // of the comment group. + if startComment < len(file.Comments) && file.Comments[startComment].Pos() <= start && start < file.Comments[startComment].End() { + start = file.Comments[startComment].End() + startComment++ + } + // Move forwards to find a non-whitespace character. + offset, err := safetoken.Offset(tok, start) + if err != nil { + return span.Range{}, err + } + for offset < len(content) && isGoWhiteSpace(content[offset]) { + offset++ } - // Move forwards one byte to find a non-whitespace character. - offset += 1 + start = tok.Pos(offset) } - rng.Start = tok.Pos(offset) - // Move backwards to find a non-whitespace character. - offset, err = safetoken.Offset(tok, rng.End) - if err != nil { - return span.Range{}, err - } - for o := offset - 1; 0 <= o && o < len(content); o-- { - if !unicode.IsSpace(rune(content[o])) { - break + // Adjust the end of the range to before trailing whitespace and comments. + prevEnd, end := token.NoPos, rng.End + endComment := sort.Search(len(file.Comments), func(i int) bool { + // Find the index for the first comment that ends after the range end. + return file.Comments[i].End() >= rng.End + }) + // Search will return n if not found, so we need to adjust if there are no + // comments that would match. + if endComment == len(file.Comments) { + endComment = -1 + } + for prevEnd != end { + prevEnd = end + // If end is within a comment, move end to the start + // of the comment group. + if endComment >= 0 && file.Comments[endComment].Pos() < end && end <= file.Comments[endComment].End() { + end = file.Comments[endComment].Pos() + endComment-- + } + // Move backwards to find a non-whitespace character. + offset, err := safetoken.Offset(tok, end) + if err != nil { + return span.Range{}, err + } + for offset > 0 && isGoWhiteSpace(content[offset-1]) { + offset-- } - offset = o + end = tok.Pos(offset) } - rng.End = tok.Pos(offset) - return rng, nil + + return span.NewRange(tok, start, end), nil +} + +// isGoWhiteSpace returns true if b is a considered white space in +// Go as defined by scanner.GoWhitespace. +func isGoWhiteSpace(b byte) bool { + return uint64(scanner.GoWhitespace)&(1<= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { @@ -117,7 +115,7 @@ func foldingRangeFunc(fset *token.FileSet, m *protocol.ColumnMapper, n ast.Node, if num := len(n.Elts); num != 0 { startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End() } - start, end = validLineFoldingRange(fset, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) + start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) } // Check that folding positions are valid. @@ -125,18 +123,18 @@ func foldingRangeFunc(fset *token.FileSet, m *protocol.ColumnMapper, n ast.Node, return nil } // in line folding mode, do not fold if the start and end lines are the same. - if lineFoldingOnly && fset.Position(start).Line == fset.Position(end).Line { + if lineFoldingOnly && tokFile.Line(start) == tokFile.Line(end) { return nil } return &FoldingRangeInfo{ - MappedRange: NewMappedRange(fset, m, start, end), + MappedRange: NewMappedRange(tokFile, m, start, end), Kind: kind, } } // validLineFoldingRange returns start and end token.Pos for folding range if the range is valid. // returns token.NoPos otherwise, which fails token.IsValid check -func validLineFoldingRange(fset *token.FileSet, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { +func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { if lineFoldingOnly { if !open.IsValid() || !close.IsValid() { return token.NoPos, token.NoPos @@ -146,8 +144,8 @@ func validLineFoldingRange(fset *token.FileSet, open, close, start, end token.Po // as an example, the example below should *not* fold: // var x = [2]string{"d", // "e" } - if fset.Position(open).Line == fset.Position(start).Line || - fset.Position(close).Line == fset.Position(end).Line { + if tokFile.Line(open) == tokFile.Line(start) || + tokFile.Line(close) == tokFile.Line(end) { return token.NoPos, token.NoPos } @@ -159,25 +157,25 @@ func validLineFoldingRange(fset *token.FileSet, open, close, start, end token.Po // commentsFoldingRange returns the folding ranges for all comment blocks in file. // The folding range starts at the end of the first line of the comment block, and ends at the end of the // comment block and has kind protocol.Comment. -func commentsFoldingRange(fset *token.FileSet, m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) { +func commentsFoldingRange(tokFile *token.File, m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) { for _, commentGrp := range file.Comments { - startGrp, endGrp := fset.Position(commentGrp.Pos()), fset.Position(commentGrp.End()) - if startGrp.Line == endGrp.Line { + startGrpLine, endGrpLine := tokFile.Line(commentGrp.Pos()), tokFile.Line(commentGrp.End()) + if startGrpLine == endGrpLine { // Don't fold single line comments. continue } firstComment := commentGrp.List[0] startPos, endLinePos := firstComment.Pos(), firstComment.End() - startCmmnt, endCmmnt := fset.Position(startPos), fset.Position(endLinePos) - if startCmmnt.Line != endCmmnt.Line { + startCmmntLine, endCmmntLine := tokFile.Line(startPos), tokFile.Line(endLinePos) + if startCmmntLine != endCmmntLine { // If the first comment spans multiple lines, then we want to have the // folding range start at the end of the first line. endLinePos = token.Pos(int(startPos) + len(strings.Split(firstComment.Text, "\n")[0])) } comments = append(comments, &FoldingRangeInfo{ // Fold from the end of the first line comment to the end of the comment block. - MappedRange: NewMappedRange(fset, m, endLinePos, commentGrp.End()), + MappedRange: NewMappedRange(tokFile, m, endLinePos, commentGrp.End()), Kind: protocol.Comment, }) } diff --git a/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go similarity index 84% rename from internal/lsp/source/format.go rename to gopls/internal/lsp/source/format.go index 1dd914ec3ff..b713893fbab 100644 --- a/internal/lsp/source/format.go +++ b/gopls/internal/lsp/source/format.go @@ -16,12 +16,12 @@ import ( "strings" "text/scanner" + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/lsppos" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" ) // Format formats a file with a given range. @@ -199,11 +199,8 @@ func computeFixEdits(snapshot Snapshot, pgf *ParsedGoFile, options *imports.Opti if fixedData == nil || fixedData[len(fixedData)-1] != '\n' { fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure. } - edits, err := snapshot.View().Options().ComputeEdits(pgf.URI, left, string(fixedData)) - if err != nil { - return nil, err - } - return ProtocolEditsFromSource([]byte(left), edits, pgf.Mapper.TokFile) + edits := snapshot.View().Options().ComputeEdits(left, string(fixedData)) + return protocolEditsFromSource([]byte(left), edits, pgf.Mapper.TokFile) } // importPrefix returns the prefix of the given file content through the final @@ -312,73 +309,85 @@ func computeTextEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, _, done := event.Start(ctx, "source.computeTextEdits") defer done() - edits, err := snapshot.View().Options().ComputeEdits(pgf.URI, string(pgf.Src), formatted) - if err != nil { - return nil, err - } + edits := snapshot.View().Options().ComputeEdits(string(pgf.Src), formatted) return ToProtocolEdits(pgf.Mapper, edits) } -// ProtocolEditsFromSource converts text edits to LSP edits using the original +// protocolEditsFromSource converts text edits to LSP edits using the original // source. -func ProtocolEditsFromSource(src []byte, edits []diff.TextEdit, tf *token.File) ([]protocol.TextEdit, error) { +func protocolEditsFromSource(src []byte, edits []diff.Edit, tf *token.File) ([]protocol.TextEdit, error) { m := lsppos.NewMapper(src) var result []protocol.TextEdit for _, edit := range edits { - spn, err := edit.Span.WithOffset(tf) - if err != nil { - return nil, fmt.Errorf("computing offsets: %v", err) - } - rng, err := m.Range(spn.Start().Offset(), spn.End().Offset()) + rng, err := m.Range(edit.Start, edit.End) if err != nil { return nil, err } - if rng.Start == rng.End && edit.NewText == "" { + if rng.Start == rng.End && edit.New == "" { // Degenerate case, which may result from a diff tool wanting to delete // '\r' in line endings. Filter it out. continue } result = append(result, protocol.TextEdit{ Range: rng, - NewText: edit.NewText, + NewText: edit.New, }) } return result, nil } -func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) { - if edits == nil { - return nil, nil - } +// ToProtocolEdits converts diff.Edits to LSP TextEdits. +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray +func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.Edit) ([]protocol.TextEdit, error) { + // LSP doesn't require TextEditArray to be sorted: + // this is the receiver's concern. But govim, and perhaps + // other clients have historically relied on the order. + edits = append([]diff.Edit(nil), edits...) + diff.SortEdits(edits) + result := make([]protocol.TextEdit, len(edits)) for i, edit := range edits { - rng, err := m.Range(edit.Span) + rng, err := m.OffsetRange(edit.Start, edit.End) if err != nil { return nil, err } result[i] = protocol.TextEdit{ Range: rng, - NewText: edit.NewText, + NewText: edit.New, } } return result, nil } -func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.TextEdit, error) { +// FromProtocolEdits converts LSP TextEdits to diff.Edits. +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray +func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.Edit, error) { if edits == nil { return nil, nil } - result := make([]diff.TextEdit, len(edits)) + result := make([]diff.Edit, len(edits)) for i, edit := range edits { spn, err := m.RangeSpan(edit.Range) if err != nil { return nil, err } - result[i] = diff.TextEdit{ - Span: spn, - NewText: edit.NewText, + result[i] = diff.Edit{ + Start: spn.Start().Offset(), + End: spn.End().Offset(), + New: edit.NewText, } } return result, nil } + +// ApplyProtocolEdits applies the patch (edits) to m.Content and returns the result. +// It also returns the edits converted to diff-package form. +func ApplyProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) (string, []diff.Edit, error) { + diffEdits, err := FromProtocolEdits(m, edits) + if err != nil { + return "", nil, err + } + out, err := diff.Apply(string(m.Content), diffEdits) + return out, diffEdits, err +} diff --git a/internal/lsp/source/format_test.go b/gopls/internal/lsp/source/format_test.go similarity index 74% rename from internal/lsp/source/format_test.go rename to gopls/internal/lsp/source/format_test.go index eac78d97989..fac80c3115b 100644 --- a/internal/lsp/source/format_test.go +++ b/gopls/internal/lsp/source/format_test.go @@ -5,12 +5,10 @@ package source import ( - "fmt" "strings" "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) func TestImportPrefix(t *testing.T) { @@ -39,8 +37,8 @@ func TestImportPrefix(t *testing.T) { if err != nil { t.Fatal(err) } - if got != tt.want { - t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(t, tt.want, got)) + if d := compare.Text(tt.want, got); d != "" { + t.Errorf("%d: failed for %q:\n%s", i, tt.input, d) } } } @@ -70,22 +68,8 @@ Hi description t.Fatal(err) } want := strings.ReplaceAll(tt.want, "\n", "\r\n") - if got != want { - t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(t, want, got)) + if d := compare.Text(want, got); d != "" { + t.Errorf("%d: failed for %q:\n%s", i, tt.input, d) } } } - -func diffStr(t *testing.T, want, got string) string { - if want == got { - return "" - } - // Add newlines to avoid newline messages in diff. - want += "\n" - got += "\n" - d, err := myers.ComputeEdits("", want, got) - if err != nil { - t.Fatal(err) - } - return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d)) -} diff --git a/internal/lsp/source/gc_annotations.go b/gopls/internal/lsp/source/gc_annotations.go similarity index 98% rename from internal/lsp/source/gc_annotations.go rename to gopls/internal/lsp/source/gc_annotations.go index 3616bbfb1cf..ab0fd6035e6 100644 --- a/internal/lsp/source/gc_annotations.go +++ b/gopls/internal/lsp/source/gc_annotations.go @@ -14,9 +14,9 @@ import ( "path/filepath" "strings" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" ) type Annotation string diff --git a/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go similarity index 99% rename from internal/lsp/source/highlight.go rename to gopls/internal/lsp/source/highlight.go index 4be078b7fce..5ec71d075a8 100644 --- a/internal/lsp/source/highlight.go +++ b/gopls/internal/lsp/source/highlight.go @@ -13,8 +13,8 @@ import ( "strings" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" ) func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Range, error) { @@ -59,7 +59,7 @@ func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position p } var ranges []protocol.Range for rng := range result { - mRng, err := posToMappedRange(snapshot, pkg, rng.start, rng.end) + mRng, err := posToMappedRange(snapshot.FileSet(), pkg, rng.start, rng.end) if err != nil { return nil, err } diff --git a/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go similarity index 86% rename from internal/lsp/source/hover.go rename to gopls/internal/lsp/source/hover.go index 58ea9696203..09f7224c80d 100644 --- a/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -21,10 +21,11 @@ import ( "unicode/utf8" "golang.org/x/text/unicode/runenames" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" "golang.org/x/tools/internal/typeparams" ) @@ -236,7 +237,7 @@ func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position pr return 0, MappedRange{}, ErrNoRuneFound } - mappedRange, err := posToMappedRange(snapshot, pkg, start, end) + mappedRange, err := posToMappedRange(snapshot.FileSet(), pkg, start, end) if err != nil { return 0, MappedRange{}, err } @@ -273,6 +274,24 @@ func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) if err := format.Node(&b, fset, &x2); err != nil { return nil, err } + + // Display the declared methods accessible from the identifier. + // + // (The format.Node call above displays any struct fields, public + // or private, in syntactic form. We choose not to recursively + // enumerate any fields and methods promoted from them.) + obj := i.Type.Object + if obj != nil && !types.IsInterface(obj.Type()) { + sep := "\n\n" + for _, m := range typeutil.IntuitiveMethodSet(obj.Type(), nil) { + if (m.Obj().Exported() || m.Obj().Pkg() == i.pkg.GetTypes()) && len(m.Index()) == 1 { + b.WriteString(sep) + sep = "\n" + b.WriteString(objectString(m.Obj(), i.qf, nil)) + } + } + } + h.Signature = b.String() case ast.Node: @@ -323,7 +342,7 @@ func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) // See golang/go#36998: don't link to modules matching GOPRIVATE. // - // The path returned by linkData is an import path. + // The path returned by linkData is a package path. if i.Snapshot.View().IsGoPrivatePath(h.LinkPath) { h.LinkPath = "" } else if mod, version, ok := moduleAtVersion(h.LinkPath, i); ok { @@ -333,11 +352,11 @@ func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverJSON, error) return h, nil } -// linkData returns the name, import path, and anchor to use in building links +// linkData returns the name, package path, and anchor to use in building links // to obj. // // If obj is not visible in documentation, the returned name will be empty. -func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, anchor string) { +func linkData(obj types.Object, enclosing *types.TypeName) (name, packagePath, anchor string) { // Package names simply link to the package. if obj, ok := obj.(*types.PkgName); ok { return obj.Name(), obj.Imported().Path(), "" @@ -411,7 +430,7 @@ func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, an return "", "", "" } - importPath = obj.Pkg().Path() + packagePath = obj.Pkg().Path() if recv != nil { anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name()) name = fmt.Sprintf("(%s.%s).%s", obj.Pkg().Name(), recv.Name(), obj.Name()) @@ -420,7 +439,7 @@ func linkData(obj types.Object, enclosing *types.TypeName) (name, importPath, an anchor = obj.Name() name = fmt.Sprintf("%s.%s", obj.Pkg().Name(), obj.Name()) } - return name, importPath, anchor + return name, packagePath, anchor } func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { @@ -429,7 +448,7 @@ func moduleAtVersion(path string, i *IdentifierInfo) (string, string, bool) { if strings.ToLower(i.Snapshot.View().Options().LinkTarget) != "pkg.go.dev" { return "", "", false } - impPkg, err := i.pkg.GetImport(path) + impPkg, err := i.pkg.DirectDep(path) if err != nil { return "", "", false } @@ -516,11 +535,11 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob } case *ast.ImportSpec: // Try to find the package documentation for an imported package. - pkgPath, err := strconv.Unquote(node.Path.Value) + importPath, err := strconv.Unquote(node.Path.Value) if err != nil { return nil, err } - imp, err := pkg.GetImport(pkgPath) + imp, err := pkg.ResolveImportPath(importPath) if err != nil { return nil, err } @@ -610,11 +629,7 @@ func FindHoverContext(ctx context.Context, s Snapshot, pkg Package, obj types.Ob break } - field, err := s.PosToField(ctx, pkg, obj.Pos()) - if err != nil { - return nil, err - } - + _, field := FindDeclAndField(pkg.GetSyntax(), obj.Pos()) if field != nil { comment := field.Doc if comment.Text() == "" { @@ -801,12 +816,8 @@ func FormatHover(h *HoverJSON, options *Options) (string, error) { if el != "" { b.WriteString(el) - // Don't write out final newline. - if i == len(parts) { - continue - } // If any elements of the remainder of the list are non-empty, - // write a newline. + // write an extra newline. if anyNonEmpty(parts[i+1:]) { if options.PreferredContentFormat == protocol.Markdown { b.WriteString("\n\n") @@ -845,9 +856,6 @@ func formatLink(h *HoverJSON, options *Options) string { // BuildLink constructs a link with the given target, path, and anchor. func BuildLink(target, path, anchor string) string { link := fmt.Sprintf("https://%s/%s", target, path) - if target == "pkg.go.dev" { - link += "?utm_source=gopls" - } if anchor == "" { return link } @@ -876,3 +884,101 @@ func anyNonEmpty(x []string) bool { } return false } + +// FindDeclAndField returns the var/func/type/const Decl that declares +// the identifier at pos, searching the given list of file syntax +// trees. If pos is the position of an ast.Field or one of its Names +// or Ellipsis.Elt, the field is returned, along with the innermost +// enclosing Decl, which could be only loosely related---consider: +// +// var decl = f( func(field int) {} ) +// +// It returns (nil, nil) if no Field or Decl is found at pos. +func FindDeclAndField(files []*ast.File, pos token.Pos) (decl ast.Decl, field *ast.Field) { + // panic(nil) breaks off the traversal and + // causes the function to return normally. + defer func() { + if x := recover(); x != nil { + panic(x) + } + }() + + // Visit the files in search of the node at pos. + stack := make([]ast.Node, 0, 20) + // Allocate the closure once, outside the loop. + f := func(n ast.Node) bool { + if n != nil { + stack = append(stack, n) // push + } else { + stack = stack[:len(stack)-1] // pop + return false + } + + // Skip subtrees (incl. files) that don't contain the search point. + if !(n.Pos() <= pos && pos < n.End()) { + return false + } + + switch n := n.(type) { + case *ast.Field: + checkField := func(f ast.Node) { + if f.Pos() == pos { + field = n + for i := len(stack) - 1; i >= 0; i-- { + if d, ok := stack[i].(ast.Decl); ok { + decl = d // innermost enclosing decl + break + } + } + panic(nil) // found + } + } + + // Check *ast.Field itself. This handles embedded + // fields which have no associated *ast.Ident name. + checkField(n) + + // Check each field name since you can have + // multiple names for the same type expression. + for _, name := range n.Names { + checkField(name) + } + + // Also check "X" in "...X". This makes it easy + // to format variadic signature params properly. + if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil { + checkField(ell.Elt) + } + + case *ast.FuncDecl: + if n.Name.Pos() == pos { + decl = n + panic(nil) // found + } + + case *ast.GenDecl: + for _, spec := range n.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + if spec.Name.Pos() == pos { + decl = n + panic(nil) // found + } + case *ast.ValueSpec: + for _, id := range spec.Names { + if id.Pos() == pos { + decl = n + panic(nil) // found + } + } + } + } + } + return true + } + for _, file := range files { + ast.Inspect(file, f) + } + + return nil, nil +} diff --git a/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go similarity index 86% rename from internal/lsp/source/identifier.go rename to gopls/internal/lsp/source/identifier.go index 40655e20779..f11817f5865 100644 --- a/internal/lsp/source/identifier.go +++ b/gopls/internal/lsp/source/identifier.go @@ -12,15 +12,14 @@ import ( "go/parser" "go/token" "go/types" - "sort" "strconv" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/typeparams" ) @@ -81,40 +80,22 @@ func Identifier(ctx context.Context, snapshot Snapshot, fh FileHandle, position ctx, done := event.Start(ctx, "source.Identifier") defer done() - pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), TypecheckAll, false) + pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckFull, NarrowestPackage) if err != nil { return nil, err } - if len(pkgs) == 0 { - return nil, fmt.Errorf("no packages for file %v", fh.URI()) + pgf, err := pkg.File(fh.URI()) + if err != nil { + // We shouldn't get a package from PackagesForFile that doesn't actually + // contain the file. + bug.Report("missing package file", bug.Data{"pkg": pkg.ID(), "file": fh.URI()}) + return nil, err } - sort.Slice(pkgs, func(i, j int) bool { - // Prefer packages with a more complete parse mode. - if pkgs[i].ParseMode() != pkgs[j].ParseMode() { - return pkgs[i].ParseMode() > pkgs[j].ParseMode() - } - return len(pkgs[i].CompiledGoFiles()) < len(pkgs[j].CompiledGoFiles()) - }) - var findErr error - for _, pkg := range pkgs { - pgf, err := pkg.File(fh.URI()) - if err != nil { - // We shouldn't get a package from PackagesForFile that doesn't actually - // contain the file. - bug.Report("missing package file", bug.Data{"pkg": pkg.ID(), "file": fh.URI()}) - return nil, err - } - pos, err := pgf.Mapper.Pos(position) - if err != nil { - return nil, err - } - var ident *IdentifierInfo - ident, findErr = findIdentifier(ctx, snapshot, pkg, pgf, pos) - if findErr == nil { - return ident, nil - } + pos, err := pgf.Mapper.Pos(position) + if err != nil { + return nil, err } - return nil, findErr + return findIdentifier(ctx, snapshot, pkg, pgf, pos) } // ErrNoIdentFound is error returned when no identifier is found at a particular position @@ -141,7 +122,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // Special case for package declarations, since they have no // corresponding types.Object. if ident == file.Name { - rng, err := posToMappedRange(snapshot, pkg, file.Name.Pos(), file.Name.End()) + rng, err := posToMappedRange(snapshot.FileSet(), pkg, file.Name.Pos(), file.Name.End()) if err != nil { return nil, err } @@ -155,7 +136,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if declAST == nil { declAST = file } - declRng, err := posToMappedRange(snapshot, pkg, declAST.Name.Pos(), declAST.Name.End()) + declRng, err := posToMappedRange(snapshot.FileSet(), pkg, declAST.Name.Pos(), declAST.Name.End()) if err != nil { return nil, err } @@ -183,7 +164,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa result.Name = result.ident.Name var err error - if result.MappedRange, err = posToMappedRange(snapshot, pkg, result.ident.Pos(), result.ident.End()); err != nil { + if result.MappedRange, err = posToMappedRange(snapshot.FileSet(), pkg, result.ident.Pos(), result.ident.End()); err != nil { return nil, err } @@ -226,7 +207,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa // The builtin package isn't in the dependency graph, so the usual // utilities won't work here. - rng := NewMappedRange(snapshot.FileSet(), builtin.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) + rng := NewMappedRange(builtin.Tok, builtin.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name))) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } @@ -267,7 +248,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } name := method.Names[0].Name result.Declaration.node = method - rng := NewMappedRange(snapshot.FileSet(), builtin.Mapper, method.Pos(), method.Pos()+token.Pos(len(name))) + rng := NewMappedRange(builtin.Tok, builtin.Mapper, method.Pos(), method.Pos()+token.Pos(len(name))) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) return result, nil } @@ -282,19 +263,18 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa } } - rng, err := objToMappedRange(snapshot, pkg, result.Declaration.obj) + rng, err := objToMappedRange(snapshot.FileSet(), pkg, result.Declaration.obj) if err != nil { return nil, err } result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) - declPkg, err := FindPackageFromPos(ctx, snapshot, result.Declaration.obj.Pos()) + declPkg, err := FindPackageFromPos(snapshot.FileSet(), pkg, result.Declaration.obj.Pos()) if err != nil { return nil, err } - if result.Declaration.node, err = snapshot.PosToDecl(ctx, declPkg, result.Declaration.obj.Pos()); err != nil { - return nil, err - } + result.Declaration.node, _ = FindDeclAndField(declPkg.GetSyntax(), result.Declaration.obj.Pos()) // may be nil + // Ensure that we have the full declaration, in case the declaration was // parsed in ParseExported and therefore could be missing information. if result.Declaration.fullDecl, err = fullNode(snapshot, result.Declaration.obj, declPkg); err != nil { @@ -313,7 +293,7 @@ func findIdentifier(ctx context.Context, snapshot Snapshot, pkg Package, pgf *Pa if hasErrorType(result.Type.Object) { return result, nil } - if result.Type.MappedRange, err = objToMappedRange(snapshot, pkg, result.Type.Object); err != nil { + if result.Type.MappedRange, err = objToMappedRange(snapshot.FileSet(), pkg, result.Type.Object); err != nil { return nil, err } } @@ -476,22 +456,22 @@ func importSpec(snapshot Snapshot, pkg Package, file *ast.File, pos token.Pos) ( if err != nil { return nil, fmt.Errorf("import path not quoted: %s (%v)", imp.Path.Value, err) } + imported, err := pkg.ResolveImportPath(importPath) + if err != nil { + return nil, err + } result := &IdentifierInfo{ Snapshot: snapshot, - Name: importPath, + Name: importPath, // should this perhaps be imported.PkgPath()? pkg: pkg, } - if result.MappedRange, err = posToMappedRange(snapshot, pkg, imp.Path.Pos(), imp.Path.End()); err != nil { + if result.MappedRange, err = posToMappedRange(snapshot.FileSet(), pkg, imp.Path.Pos(), imp.Path.End()); err != nil { return nil, err } // Consider the "declaration" of an import spec to be the imported package. - importedPkg, err := pkg.GetImport(importPath) - if err != nil { - return nil, err - } // Return all of the files in the package as the definition of the import spec. - for _, dst := range importedPkg.GetSyntax() { - rng, err := posToMappedRange(snapshot, pkg, dst.Pos(), dst.End()) + for _, dst := range imported.GetSyntax() { + rng, err := posToMappedRange(snapshot.FileSet(), pkg, dst.Pos(), dst.End()) if err != nil { return nil, err } diff --git a/internal/lsp/source/identifier_test.go b/gopls/internal/lsp/source/identifier_test.go similarity index 100% rename from internal/lsp/source/identifier_test.go rename to gopls/internal/lsp/source/identifier_test.go diff --git a/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go similarity index 71% rename from internal/lsp/source/implementation.go rename to gopls/internal/lsp/source/implementation.go index 6666605a99a..2da488dd67a 100644 --- a/internal/lsp/source/implementation.go +++ b/gopls/internal/lsp/source/implementation.go @@ -13,10 +13,10 @@ import ( "go/types" "sort" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/span" ) func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) ([]protocol.Location, error) { @@ -32,7 +32,7 @@ func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp pro if impl.pkg == nil || len(impl.pkg.CompiledGoFiles()) == 0 { continue } - rng, err := objToMappedRange(snapshot, impl.pkg, impl.obj) + rng, err := objToMappedRange(snapshot.FileSet(), impl.pkg, impl.obj) if err != nil { return nil, err } @@ -59,23 +59,48 @@ var ErrNotAType = errors.New("not a type name or method") // implementations returns the concrete implementations of the specified // interface, or the interfaces implemented by the specified concrete type. +// It populates only the definition-related fields of qualifiedObject. +// (Arguably it should return a smaller data type.) func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) ([]qualifiedObject, error) { + // Find all named types, even local types + // (which can have methods due to promotion). var ( - impls []qualifiedObject - seen = make(map[token.Position]bool) - fset = s.FileSet() + allNamed []*types.Named + pkgs = make(map[*types.Package]Package) ) + knownPkgs, err := s.KnownPackages(ctx) + if err != nil { + return nil, err + } + for _, pkg := range knownPkgs { + pkgs[pkg.GetTypes()] = pkg + for _, obj := range pkg.GetTypesInfo().Defs { + obj, ok := obj.(*types.TypeName) + // We ignore aliases 'type M = N' to avoid duplicate reporting + // of the Named type N. + if !ok || obj.IsAlias() { + continue + } + if named, ok := obj.Type().(*types.Named); ok { + allNamed = append(allNamed, named) + } + } + } qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) if err != nil { return nil, err } + var ( + impls []qualifiedObject + seen = make(map[token.Position]bool) + ) for _, qo := range qos { + // Ascertain the query identifier (type or method). var ( queryType types.Type queryMethod *types.Func ) - switch obj := qo.obj.(type) { case *types.Func: queryMethod = obj @@ -94,32 +119,6 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. return nil, nil } - // Find all named types, even local types (which can have methods - // due to promotion). - var ( - allNamed []*types.Named - pkgs = make(map[*types.Package]Package) - ) - knownPkgs, err := s.KnownPackages(ctx) - if err != nil { - return nil, err - } - for _, pkg := range knownPkgs { - pkgs[pkg.GetTypes()] = pkg - info := pkg.GetTypesInfo() - for _, obj := range info.Defs { - obj, ok := obj.(*types.TypeName) - // We ignore aliases 'type M = N' to avoid duplicate reporting - // of the Named type N. - if !ok || obj.IsAlias() { - continue - } - if named, ok := obj.Type().(*types.Named); ok { - allNamed = append(allNamed, named) - } - } - } - // Find all the named types that match our query. for _, named := range allNamed { var ( @@ -146,7 +145,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. candObj = sel.Obj() } - pos := fset.Position(candObj.Pos()) + pos := s.FileSet().Position(candObj.Pos()) if candObj == queryMethod || seen[pos] { continue } @@ -155,7 +154,7 @@ func implementations(ctx context.Context, s Snapshot, f FileHandle, pp protocol. impls = append(impls, qualifiedObject{ obj: candObj, - pkg: pkgs[candObj.Pkg()], + pkg: pkgs[candObj.Pkg()], // may be nil (e.g. error) }) } } @@ -192,17 +191,16 @@ func ensurePointer(T types.Type) types.Type { return T } +// A qualifiedObject is the result of resolving a reference from an +// identifier to an object. type qualifiedObject struct { - obj types.Object + // definition + obj types.Object // the referenced object + pkg Package // the Package that defines the object (nil => universe) - // pkg is the Package that contains obj's definition. - pkg Package - - // node is the *ast.Ident or *ast.ImportSpec we followed to find obj, if any. - node ast.Node - - // sourcePkg is the Package that contains node, if any. - sourcePkg Package + // reference (optional) + node ast.Node // the reference (*ast.Ident or *ast.ImportSpec) to the object + sourcePkg Package // the Package containing node } var ( @@ -210,42 +208,47 @@ var ( errNoObjectFound = errors.New("no object found") ) -// qualifiedObjsAtProtocolPos returns info for all the type.Objects -// referenced at the given position. An object will be returned for -// every package that the file belongs to, in every typechecking mode -// applicable. +// qualifiedObjsAtProtocolPos returns info for all the types.Objects referenced +// at the given position, for the following selection of packages: +// +// 1. all packages (including all test variants), in their workspace parse mode +// 2. if not included above, at least one package containing uri in full parse mode +// +// Finding objects in (1) ensures that we locate references within all +// workspace packages, including in x_test packages. Including (2) ensures that +// we find local references in the current package, for non-workspace packages +// that may be open. func qualifiedObjsAtProtocolPos(ctx context.Context, s Snapshot, uri span.URI, pp protocol.Position) ([]qualifiedObject, error) { - pkgs, err := s.PackagesForFile(ctx, uri, TypecheckAll, false) - if err != nil { - return nil, err - } - if len(pkgs) == 0 { - return nil, errNoObjectFound - } - pkg := pkgs[0] - pgf, err := pkg.File(uri) + fh, err := s.GetFile(ctx, uri) if err != nil { return nil, err } - pos, err := pgf.Mapper.Pos(pp) + content, err := fh.Read() if err != nil { return nil, err } - offset, err := safetoken.Offset(pgf.Tok, pos) + m := protocol.NewColumnMapper(uri, content) + offset, err := m.Offset(pp) if err != nil { return nil, err } - return qualifiedObjsAtLocation(ctx, s, objSearchKey{uri, offset}, map[objSearchKey]bool{}) + return qualifiedObjsAtLocation(ctx, s, positionKey{uri, offset}, map[positionKey]bool{}) } -type objSearchKey struct { +// A positionKey identifies a byte offset within a file (URI). +// +// When a file has been parsed multiple times in the same FileSet, +// there may be multiple token.Pos values denoting the same logical +// position. In such situations, a positionKey may be used for +// de-duplication. +type positionKey struct { uri span.URI offset int } -// qualifiedObjsAtLocation finds all objects referenced at offset in uri, across -// all packages in the snapshot. -func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key objSearchKey, seen map[objSearchKey]bool) ([]qualifiedObject, error) { +// qualifiedObjsAtLocation finds all objects referenced at offset in uri, +// across all packages in the snapshot. +func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key positionKey, seen map[positionKey]bool) ([]qualifiedObject, error) { if seen[key] { return nil, nil } @@ -260,11 +263,31 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key objSearchKey, // try to be comprehensive in case we ever support variations on build // constraints. - pkgs, err := s.PackagesForFile(ctx, key.uri, TypecheckAll, false) + pkgs, err := s.PackagesForFile(ctx, key.uri, TypecheckWorkspace, true) if err != nil { return nil, err } + // In order to allow basic references/rename/implementations to function when + // non-workspace packages are open, ensure that we have at least one fully + // parsed package for the current file. This allows us to find references + // inside the open package. Use WidestPackage to capture references in test + // files. + hasFullPackage := false + for _, pkg := range pkgs { + if pkg.ParseMode() == ParseFull { + hasFullPackage = true + break + } + } + if !hasFullPackage { + pkg, err := s.PackageForFile(ctx, key.uri, TypecheckFull, WidestPackage) + if err != nil { + return nil, err + } + pkgs = append(pkgs, pkg) + } + // report objects in the order we encounter them. This ensures that the first // result is at the cursor... var qualifiedObjs []qualifiedObject @@ -343,21 +366,8 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key objSearchKey, // is in another package, but this should be good enough to find all // uses. - pos := obj.Pos() - var uri span.URI - offset := -1 - for _, pgf := range pkg.CompiledGoFiles() { - if pgf.Tok.Base() <= int(pos) && int(pos) <= pgf.Tok.Base()+pgf.Tok.Size() { - var err error - offset, err = safetoken.Offset(pgf.Tok, pos) - if err != nil { - return nil, err - } - uri = pgf.URI - } - } - if offset >= 0 { - otherObjs, err := qualifiedObjsAtLocation(ctx, s, objSearchKey{uri, offset}, seen) + if key, found := packagePositionKey(pkg, obj.Pos()); found { + otherObjs, err := qualifiedObjsAtLocation(ctx, s, key, seen) if err != nil { return nil, err } @@ -380,6 +390,19 @@ func qualifiedObjsAtLocation(ctx context.Context, s Snapshot, key objSearchKey, return qualifiedObjs, nil } +// packagePositionKey finds the positionKey for the given pos. +// +// The second result reports whether the position was found. +func packagePositionKey(pkg Package, pos token.Pos) (positionKey, bool) { + for _, pgf := range pkg.CompiledGoFiles() { + offset, err := safetoken.Offset(pgf.Tok, pos) + if err == nil { + return positionKey{pgf.URI, offset}, true + } + } + return positionKey{}, false +} + // pathEnclosingObjNode returns the AST path to the object-defining // node associated with pos. "Object-defining" means either an // *ast.Ident mapped directly to a types.Object or an ast.Node mapped diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go new file mode 100644 index 00000000000..9d9152f87e8 --- /dev/null +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -0,0 +1,396 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "fmt" + "go/ast" + "go/constant" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/internal/event" + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/typeparams" +) + +const ( + maxLabelLength = 28 +) + +type InlayHintFunc func(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint + +type Hint struct { + Name string + Doc string + Run InlayHintFunc +} + +const ( + ParameterNames = "parameterNames" + AssignVariableTypes = "assignVariableTypes" + ConstantValues = "constantValues" + RangeVariableTypes = "rangeVariableTypes" + CompositeLiteralTypes = "compositeLiteralTypes" + CompositeLiteralFieldNames = "compositeLiteralFields" + FunctionTypeParameters = "functionTypeParameters" +) + +var AllInlayHints = map[string]*Hint{ + AssignVariableTypes: { + Name: AssignVariableTypes, + Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", + Run: assignVariableTypes, + }, + ParameterNames: { + Name: ParameterNames, + Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", + Run: parameterNames, + }, + ConstantValues: { + Name: ConstantValues, + Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", + Run: constantValues, + }, + RangeVariableTypes: { + Name: RangeVariableTypes, + Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", + Run: rangeVariableTypes, + }, + CompositeLiteralTypes: { + Name: CompositeLiteralTypes, + Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", + Run: compositeLiteralTypes, + }, + CompositeLiteralFieldNames: { + Name: CompositeLiteralFieldNames, + Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", + Run: compositeLiteralFields, + }, + FunctionTypeParameters: { + Name: FunctionTypeParameters, + Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", + Run: funcTypeParams, + }, +} + +func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.InlayHint, error) { + ctx, done := event.Start(ctx, "source.InlayHint") + defer done() + + pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) + if err != nil { + return nil, fmt.Errorf("getting file for InlayHint: %w", err) + } + + // Collect a list of the inlay hints that are enabled. + inlayHintOptions := snapshot.View().Options().InlayHintOptions + var enabledHints []InlayHintFunc + for hint, enabled := range inlayHintOptions.Hints { + if !enabled { + continue + } + if h, ok := AllInlayHints[hint]; ok { + enabledHints = append(enabledHints, h.Run) + } + } + if len(enabledHints) == 0 { + return nil, nil + } + + tmap := lsppos.NewTokenMapper(pgf.Src, pgf.Tok) + info := pkg.GetTypesInfo() + q := Qualifier(pgf.File, pkg.GetTypes(), info) + + // Set the range to the full file if the range is not valid. + start, end := pgf.File.Pos(), pgf.File.End() + if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character { + // Adjust start and end for the specified range. + rng, err := pgf.Mapper.RangeToSpanRange(pRng) + if err != nil { + return nil, err + } + start, end = rng.Start, rng.End + } + + var hints []protocol.InlayHint + ast.Inspect(pgf.File, func(node ast.Node) bool { + // If not in range, we can stop looking. + if node == nil || node.End() < start || node.Pos() > end { + return false + } + for _, fn := range enabledHints { + hints = append(hints, fn(node, tmap, info, &q)...) + } + return true + }) + return hints, nil +} + +func parameterNames(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { + callExpr, ok := node.(*ast.CallExpr) + if !ok { + return nil + } + signature, ok := info.TypeOf(callExpr.Fun).(*types.Signature) + if !ok { + return nil + } + + var hints []protocol.InlayHint + for i, v := range callExpr.Args { + start, ok := tmap.Position(v.Pos()) + if !ok { + continue + } + params := signature.Params() + // When a function has variadic params, we skip args after + // params.Len(). + if i > params.Len()-1 { + break + } + param := params.At(i) + // param.Name is empty for built-ins like append + if param.Name() == "" { + continue + } + // Skip the parameter name hint if the arg matches the + // the parameter name. + if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() { + continue + } + + label := param.Name() + if signature.Variadic() && i == params.Len()-1 { + label = label + "..." + } + hints = append(hints, protocol.InlayHint{ + Position: &start, + Label: buildLabel(label + ":"), + Kind: protocol.Parameter, + PaddingRight: true, + }) + } + return hints +} + +func funcTypeParams(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { + ce, ok := node.(*ast.CallExpr) + if !ok { + return nil + } + id, ok := ce.Fun.(*ast.Ident) + if !ok { + return nil + } + inst := typeparams.GetInstances(info)[id] + if inst.TypeArgs == nil { + return nil + } + start, ok := tmap.Position(id.End()) + if !ok { + return nil + } + var args []string + for i := 0; i < inst.TypeArgs.Len(); i++ { + args = append(args, inst.TypeArgs.At(i).String()) + } + if len(args) == 0 { + return nil + } + return []protocol.InlayHint{{ + Position: &start, + Label: buildLabel("[" + strings.Join(args, ", ") + "]"), + Kind: protocol.Type, + }} +} + +func assignVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + stmt, ok := node.(*ast.AssignStmt) + if !ok || stmt.Tok != token.DEFINE { + return nil + } + + var hints []protocol.InlayHint + for _, v := range stmt.Lhs { + if h := variableType(v, tmap, info, q); h != nil { + hints = append(hints, *h) + } + } + return hints +} + +func rangeVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + rStmt, ok := node.(*ast.RangeStmt) + if !ok { + return nil + } + var hints []protocol.InlayHint + if h := variableType(rStmt.Key, tmap, info, q); h != nil { + hints = append(hints, *h) + } + if h := variableType(rStmt.Value, tmap, info, q); h != nil { + hints = append(hints, *h) + } + return hints +} + +func variableType(e ast.Expr, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) *protocol.InlayHint { + typ := info.TypeOf(e) + if typ == nil { + return nil + } + end, ok := tmap.Position(e.End()) + if !ok { + return nil + } + return &protocol.InlayHint{ + Position: &end, + Label: buildLabel(types.TypeString(typ, *q)), + Kind: protocol.Type, + PaddingLeft: true, + } +} + +func constantValues(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { + genDecl, ok := node.(*ast.GenDecl) + if !ok || genDecl.Tok != token.CONST { + return nil + } + + var hints []protocol.InlayHint + for _, v := range genDecl.Specs { + spec, ok := v.(*ast.ValueSpec) + if !ok { + continue + } + end, ok := tmap.Position(v.End()) + if !ok { + continue + } + // Show hints when values are missing or at least one value is not + // a basic literal. + showHints := len(spec.Values) == 0 + checkValues := len(spec.Names) == len(spec.Values) + var values []string + for i, w := range spec.Names { + obj, ok := info.ObjectOf(w).(*types.Const) + if !ok || obj.Val().Kind() == constant.Unknown { + return nil + } + if checkValues { + switch spec.Values[i].(type) { + case *ast.BadExpr: + return nil + case *ast.BasicLit: + default: + if obj.Val().Kind() != constant.Bool { + showHints = true + } + } + } + values = append(values, fmt.Sprintf("%v", obj.Val())) + } + if !showHints || len(values) == 0 { + continue + } + hints = append(hints, protocol.InlayHint{ + Position: &end, + Label: buildLabel("= " + strings.Join(values, ", ")), + PaddingLeft: true, + }) + } + return hints +} + +func compositeLiteralFields(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + compLit, ok := node.(*ast.CompositeLit) + if !ok { + return nil + } + typ := info.TypeOf(compLit) + if typ == nil { + return nil + } + if t, ok := typ.(*types.Pointer); ok { + typ = t.Elem() + } + strct, ok := typ.Underlying().(*types.Struct) + if !ok { + return nil + } + + var hints []protocol.InlayHint + var allEdits []protocol.TextEdit + for i, v := range compLit.Elts { + if _, ok := v.(*ast.KeyValueExpr); !ok { + start, ok := tmap.Position(v.Pos()) + if !ok { + continue + } + if i > strct.NumFields()-1 { + break + } + hints = append(hints, protocol.InlayHint{ + Position: &start, + Label: buildLabel(strct.Field(i).Name() + ":"), + Kind: protocol.Parameter, + PaddingRight: true, + }) + allEdits = append(allEdits, protocol.TextEdit{ + Range: protocol.Range{Start: start, End: start}, + NewText: strct.Field(i).Name() + ": ", + }) + } + } + // It is not allowed to have a mix of keyed and unkeyed fields, so + // have the text edits add keys to all fields. + for i := range hints { + hints[i].TextEdits = allEdits + } + return hints +} + +func compositeLiteralTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint { + compLit, ok := node.(*ast.CompositeLit) + if !ok { + return nil + } + typ := info.TypeOf(compLit) + if typ == nil { + return nil + } + if compLit.Type != nil { + return nil + } + prefix := "" + if t, ok := typ.(*types.Pointer); ok { + typ = t.Elem() + prefix = "&" + } + // The type for this composite literal is implicit, add an inlay hint. + start, ok := tmap.Position(compLit.Lbrace) + if !ok { + return nil + } + return []protocol.InlayHint{{ + Position: &start, + Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))), + Kind: protocol.Type, + }} +} + +func buildLabel(s string) []protocol.InlayHintLabelPart { + label := protocol.InlayHintLabelPart{ + Value: s, + } + if len(s) > maxLabelLength+len("...") { + label.Value = s[:maxLabelLength] + "..." + } + return []protocol.InlayHintLabelPart{label} +} diff --git a/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go similarity index 96% rename from internal/lsp/source/known_packages.go rename to gopls/internal/lsp/source/known_packages.go index d7f229ecc80..e2d950ba00b 100644 --- a/internal/lsp/source/known_packages.go +++ b/gopls/internal/lsp/source/known_packages.go @@ -28,6 +28,8 @@ func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandl for _, imp := range pgf.File.Imports { alreadyImported[imp.Path.Value] = struct{}{} } + // TODO(adonovan): this whole algorithm could be more + // simply expressed in terms of Metadata, not Packages. pkgs, err := snapshot.CachedImportPaths(ctx) if err != nil { return nil, err diff --git a/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go similarity index 79% rename from internal/lsp/source/options.go rename to gopls/internal/lsp/source/options.go index d1d34efe787..23d795ef0e5 100644 --- a/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -43,29 +43,30 @@ import ( "golang.org/x/tools/go/analysis/passes/structtag" "golang.org/x/tools/go/analysis/passes/testinggoroutine" "golang.org/x/tools/go/analysis/passes/tests" + "golang.org/x/tools/go/analysis/passes/timeformat" "golang.org/x/tools/go/analysis/passes/unmarshal" "golang.org/x/tools/go/analysis/passes/unreachable" "golang.org/x/tools/go/analysis/passes/unsafeptr" "golang.org/x/tools/go/analysis/passes/unusedresult" "golang.org/x/tools/go/analysis/passes/unusedwrite" - "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/analysis/embeddirective" - "golang.org/x/tools/internal/lsp/analysis/fillreturns" - "golang.org/x/tools/internal/lsp/analysis/fillstruct" - "golang.org/x/tools/internal/lsp/analysis/infertypeargs" - "golang.org/x/tools/internal/lsp/analysis/nonewvars" - "golang.org/x/tools/internal/lsp/analysis/noresultvalues" - "golang.org/x/tools/internal/lsp/analysis/simplifycompositelit" - "golang.org/x/tools/internal/lsp/analysis/simplifyrange" - "golang.org/x/tools/internal/lsp/analysis/simplifyslice" - "golang.org/x/tools/internal/lsp/analysis/stubmethods" - "golang.org/x/tools/internal/lsp/analysis/undeclaredname" - "golang.org/x/tools/internal/lsp/analysis/unusedparams" - "golang.org/x/tools/internal/lsp/analysis/useany" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/analysis/embeddirective" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns" + "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" + "golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs" + "golang.org/x/tools/gopls/internal/lsp/analysis/nonewvars" + "golang.org/x/tools/gopls/internal/lsp/analysis/noresultvalues" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifycompositelit" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyrange" + "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyslice" + "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" + "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedvariable" + "golang.org/x/tools/gopls/internal/lsp/analysis/useany" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/myers" ) var ( @@ -117,8 +118,9 @@ func DefaultOptions() *Options { ExpandWorkspaceToModule: true, ExperimentalPackageCacheKey: true, MemoryMode: ModeNormal, - DirectoryFilters: []string{"-node_modules"}, + DirectoryFilters: []string{"-**/node_modules"}, TemplateExtensions: []string{}, + StandaloneTags: []string{"ignore"}, }, UIOptions: UIOptions{ DiagnosticOptions: DiagnosticOptions{ @@ -130,6 +132,7 @@ func DefaultOptions() *Options { Nil: true, }, }, + InlayHintOptions: InlayHintOptions{}, DocumentationOptions: DocumentationOptions{ HoverKind: FullDocumentation, LinkTarget: "pkg.go.dev", @@ -152,6 +155,7 @@ func DefaultOptions() *Options { string(command.GCDetails): false, string(command.UpgradeDependency): true, string(command.Vendor): true, + // TODO(hyangah): enable command.RunVulncheckExp. }, }, }, @@ -161,8 +165,11 @@ func DefaultOptions() *Options { CompleteUnimported: true, CompletionDocumentation: true, DeepCompletion: true, + ChattyDiagnostics: true, + NewDiff: "both", }, Hooks: Hooks{ + // TODO(adonovan): switch to new diff.Strings implementation. ComputeEdits: myers.ComputeEdits, URLRegexp: urlRegexp(), DefaultAnalyzers: defaultAnalyzers(), @@ -202,6 +209,7 @@ type ClientOptions struct { RelatedInformationSupported bool CompletionTags bool CompletionDeprecated bool + SupportedResourceOperations []protocol.ResourceOperationKind } // ServerOptions holds LSP-specific configuration that is provided by the @@ -227,9 +235,13 @@ type BuildOptions struct { // the last filter that applies to a path controls whether it is included. // The path prefix can be empty, so an initial `-` excludes everything. // + // DirectoryFilters also supports the `**` operator to match 0 or more directories. + // // Examples: // - // Exclude node_modules: `-node_modules` + // Exclude node_modules at current depth: `-node_modules` + // + // Exclude node_modules at any depth: `-**/node_modules` // // Include only project_a: `-` (exclude everything), `+project_a` // @@ -257,6 +269,9 @@ type BuildOptions struct { // ExperimentalWorkspaceModule opts a user into the experimental support // for multi-module workspaces. + // + // Deprecated: this feature is deprecated and will be removed in a future + // version of gopls (https://go.dev/issue/55331). ExperimentalWorkspaceModule bool `status:"experimental"` // ExperimentalPackageCacheKey controls whether to use a coarser cache key @@ -279,9 +294,31 @@ type BuildOptions struct { // ExperimentalUseInvalidMetadata enables gopls to fall back on outdated // package metadata to provide editor features if the go command fails to - // load packages for some reason (like an invalid go.mod file). This will - // eventually be the default behavior, and this setting will be removed. + // load packages for some reason (like an invalid go.mod file). + // + // Deprecated: this setting is deprecated and will be removed in a future + // version of gopls (https://go.dev/issue/55333). ExperimentalUseInvalidMetadata bool `status:"experimental"` + + // StandaloneTags specifies a set of build constraints that identify + // individual Go source files that make up the entire main package of an + // executable. + // + // A common example of standalone main files is the convention of using the + // directive `//go:build ignore` to denote files that are not intended to be + // included in any package, for example because they are invoked directly by + // the developer using `go run`. + // + // Gopls considers a file to be a standalone main file if and only if it has + // package name "main" and has a build directive of the exact form + // "//go:build tag" or "// +build tag", where tag is among the list of tags + // configured by this setting. Notably, if the build constraint is more + // complicated than a simple tag (such as the composite constraint + // `//go:build tag && go1.18`), the file is not considered to be a standalone + // main file. + // + // This setting is only supported when gopls is built with Go 1.16 or later. + StandaloneTags []string } type UIOptions struct { @@ -289,6 +326,7 @@ type UIOptions struct { CompletionOptions NavigationOptions DiagnosticOptions + InlayHintOptions // Codelenses overrides the enabled/disabled state of code lenses. See the // "Code Lenses" section of the @@ -312,6 +350,12 @@ type UIOptions struct { // SemanticTokens controls whether the LSP server will send // semantic tokens to the client. SemanticTokens bool `status:"experimental"` + + // NoSemanticString turns off the sending of the semantic token 'string' + NoSemanticString bool `status:"experimental"` + + // NoSemanticNumber turns off the sending of the semantic token 'number' + NoSemanticNumber bool `status:"experimental"` } type CompletionOptions struct { @@ -347,6 +391,9 @@ type DocumentationOptions struct { // * `"pkg.go.dev"` // // If company chooses to use its own `godoc.org`, its address can be used as well. + // + // Modules matching the GOPRIVATE environment variable will not have + // documentation links in hover. LinkTarget string // LinksInHover toggles the presence of links to documentation in hover. @@ -367,8 +414,8 @@ type FormattingOptions struct { type DiagnosticOptions struct { // Analyses specify analyses that the user would like to enable or disable. // A map of the names of analysis passes that should be enabled/disabled. - // A full list of analyzers that gopls uses can be found - // [here](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). + // A full list of analyzers that gopls uses can be found in + // [analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). // // Example Usage: // @@ -383,6 +430,8 @@ type DiagnosticOptions struct { Analyses map[string]bool // Staticcheck enables additional analyses from staticcheck.io. + // These analyses are documented on + // [Staticcheck's website](https://staticcheck.io/docs/checks/). Staticcheck bool `status:"experimental"` // Annotations specifies the various kinds of optimization diagnostics @@ -404,9 +453,19 @@ type DiagnosticOptions struct { // file system notifications. // // This option must be set to a valid duration string, for example `"100ms"`. + // + // Deprecated: this setting is deprecated and will be removed in a future + // version of gopls (https://go.dev/issue/55332) ExperimentalWatchedFileDelay time.Duration `status:"experimental"` } +type InlayHintOptions struct { + // Hints specify inlay hints that users want to see. A full list of hints + // that gopls uses can be found in + // [inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). + Hints map[string]bool `status:"experimental"` +} + type NavigationOptions struct { // ImportShortcut specifies whether import statements should link to // documentation or go to definitions. @@ -461,20 +520,24 @@ func (u *UserOptions) SetEnvSlice(env []string) { } } +// DiffFunction is the type for a function that produces a set of edits that +// convert from the before content to the after content. +type DiffFunction func(before, after string) []diff.Edit + // Hooks contains configuration that is provided to the Gopls command by the // main package. type Hooks struct { // LicensesText holds third party licenses for software used by gopls. LicensesText string - // TODO(rfindley): is this even necessary? + // GoDiff is used in gopls/hooks to get Myers' diff GoDiff bool // Whether staticcheck is supported. StaticcheckSupported bool // ComputeEdits is used to compute edits between file versions. - ComputeEdits diff.ComputeEdits + ComputeEdits DiffFunction // URLRegexp is used to find potential URLs in comments/strings. // @@ -491,9 +554,6 @@ type Hooks struct { TypeErrorAnalyzers map[string]*Analyzer ConvenienceAnalyzers map[string]*Analyzer StaticcheckAnalyzers map[string]*Analyzer - - // Govulncheck is the implementation of the Govulncheck gopls command. - Govulncheck func(context.Context, *packages.Config, command.VulncheckArgs) (command.VulncheckResult, error) } // InternalOptions contains settings that are not intended for use by the @@ -552,6 +612,17 @@ type InternalOptions struct { // on the server. // This option applies only during initialization. ShowBugReports bool + + // NewDiff controls the choice of the new diff implementation. It can be + // 'new', 'old', or 'both', which is the default. 'both' computes diffs with + // both algorithms, checks that the new algorithm has worked, and write some + // summary statistics to a file in os.TmpDir(). + NewDiff string + + // ChattyDiagnostics controls whether to report file diagnostics for each + // file change. If unset, gopls only reports diagnostics when they change, or + // when a file is opened or closed. + ChattyDiagnostics bool } type ImportShortcut string @@ -637,16 +708,6 @@ type OptionResult struct { Error error } -type OptionState int - -const ( - OptionHandled = OptionState(iota) - OptionDeprecated - OptionUnexpected -) - -type LinkTarget string - func SetOptions(options *Options, opts interface{}) OptionResults { var results OptionResults switch opts := opts.(type) { @@ -681,6 +742,9 @@ func SetOptions(options *Options, opts interface{}) OptionResults { func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) { // Check if the client supports snippets in completion items. + if caps.Workspace.WorkspaceEdit != nil { + o.SupportedResourceOperations = caps.Workspace.WorkspaceEdit.ResourceOperations + } if c := caps.TextDocument.Completion; c.CompletionItem.SnippetSupport { o.InsertTextFormat = protocol.SnippetTextFormat } @@ -716,6 +780,8 @@ func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) { } func (o *Options) Clone() *Options { + // TODO(rfindley): has this function gone stale? It appears that there are + // settings that are incorrectly cloned here (such as TemplateExtensions). result := &Options{ ClientOptions: o.ClientOptions, InternalOptions: o.InternalOptions, @@ -725,7 +791,6 @@ func (o *Options) Clone() *Options { ComputeEdits: o.ComputeEdits, GofumptFormat: o.GofumptFormat, URLRegexp: o.URLRegexp, - Govulncheck: o.Govulncheck, }, ServerOptions: o.ServerOptions, UserOptions: o.UserOptions, @@ -750,6 +815,7 @@ func (o *Options) Clone() *Options { result.SetEnvSlice(o.EnvSlice()) result.BuildFlags = copySlice(o.BuildFlags) result.DirectoryFilters = copySlice(o.DirectoryFilters) + result.StandaloneTags = copySlice(o.StandaloneTags) copyAnalyzerMap := func(src map[string]*Analyzer) map[string]*Analyzer { dst := make(map[string]*Analyzer) @@ -778,10 +844,6 @@ func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer, enabled bool, sev // should be enabled in enableAllExperimentMaps. func (o *Options) EnableAllExperiments() { o.SemanticTokens = true - o.ExperimentalPostfixCompletions = true - o.ExperimentalUseInvalidMetadata = true - o.ExperimentalWatchedFileDelay = 50 * time.Millisecond - o.SymbolMatcher = SymbolFastFuzzy } func (o *Options) enableAllExperimentMaps() { @@ -791,6 +853,33 @@ func (o *Options) enableAllExperimentMaps() { if _, ok := o.Analyses[unusedparams.Analyzer.Name]; !ok { o.Analyses[unusedparams.Analyzer.Name] = true } + if _, ok := o.Analyses[unusedvariable.Analyzer.Name]; !ok { + o.Analyses[unusedvariable.Analyzer.Name] = true + } +} + +// validateDirectoryFilter validates if the filter string +// - is not empty +// - start with either + or - +// - doesn't contain currently unsupported glob operators: *, ? +func validateDirectoryFilter(ifilter string) (string, error) { + filter := fmt.Sprint(ifilter) + if filter == "" || (filter[0] != '+' && filter[0] != '-') { + return "", fmt.Errorf("invalid filter %v, must start with + or -", filter) + } + segs := strings.Split(filter[1:], "/") + unsupportedOps := [...]string{"?", "*"} + for _, seg := range segs { + if seg != "**" { + for _, op := range unsupportedOps { + if strings.Contains(seg, op) { + return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue.", filter, op) + } + } + } + } + + return strings.TrimRight(filepath.FromSlash(filter), "/"), nil } func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult { @@ -800,7 +889,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result := OptionResult{Name: name, Value: value} if _, ok := seen[name]; ok { - result.errorf("duplicate configuration for %s", name) + result.parseErrorf("duplicate configuration for %s", name) } seen[name] = struct{}{} @@ -808,7 +897,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "env": menv, ok := value.(map[string]interface{}) if !ok { - result.errorf("invalid type %T, expect map", value) + result.parseErrorf("invalid type %T, expect map", value) break } if o.Env == nil { @@ -819,9 +908,10 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) } case "buildFlags": + // TODO(rfindley): use asStringSlice. iflags, ok := value.([]interface{}) if !ok { - result.errorf("invalid type %T, expect list", value) + result.parseErrorf("invalid type %T, expect list", value) break } flags := make([]string, 0, len(iflags)) @@ -829,22 +919,25 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) flags = append(flags, fmt.Sprintf("%s", flag)) } o.BuildFlags = flags + case "directoryFilters": + // TODO(rfindley): use asStringSlice. ifilters, ok := value.([]interface{}) if !ok { - result.errorf("invalid type %T, expect list", value) + result.parseErrorf("invalid type %T, expect list", value) break } var filters []string for _, ifilter := range ifilters { - filter := fmt.Sprint(ifilter) - if filter == "" || (filter[0] != '+' && filter[0] != '-') { - result.errorf("invalid filter %q, must start with + or -", filter) + filter, err := validateDirectoryFilter(fmt.Sprintf("%v", ifilter)) + if err != nil { + result.parseErrorf("%v", err) return result } filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/")) } o.DirectoryFilters = filters + case "memoryMode": if s, ok := result.asOneOf( string(ModeNormal), @@ -915,6 +1008,9 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "analyses": result.setBoolMap(&o.Analyses) + case "hints": + result.setBoolMap(&o.Hints) + case "annotations": result.setAnnotationMap(&o.Annotations) @@ -940,10 +1036,8 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) if v, ok := result.asBool(); ok { o.Staticcheck = v if v && !o.StaticcheckSupported { - // Warn if the user is trying to enable staticcheck, but staticcheck is - // unsupported. - result.Error = fmt.Errorf("applying setting %q: staticcheck is not supported at %s\n"+ - "\trebuild gopls with a more recent version of Go", result.Name, runtime.Version()) + result.Error = fmt.Errorf("applying setting %q: staticcheck is not supported at %s;"+ + " rebuild gopls with a more recent version of Go", result.Name, runtime.Version()) } } @@ -963,18 +1057,35 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.ShowBugReports) case "gofumpt": - result.setBool(&o.Gofumpt) + if v, ok := result.asBool(); ok { + o.Gofumpt = v + if v && o.GofumptFormat == nil { + result.Error = fmt.Errorf("applying setting %q: gofumpt is not supported at %s;"+ + " rebuild gopls with a more recent version of Go", result.Name, runtime.Version()) + } + } case "semanticTokens": result.setBool(&o.SemanticTokens) + case "noSemanticString": + result.setBool(&o.NoSemanticString) + + case "noSemanticNumber": + result.setBool(&o.NoSemanticNumber) + case "expandWorkspaceToModule": result.setBool(&o.ExpandWorkspaceToModule) case "experimentalPostfixCompletions": result.setBool(&o.ExperimentalPostfixCompletions) - case "experimentalWorkspaceModule": // TODO(rfindley): suggest go.work on go1.18+ + case "experimentalWorkspaceModule": + const msg = "experimentalWorkspaceModule has been replaced by go workspaces, " + + "and will be removed in a future version of gopls (https://go.dev/issue/55331) -- " + + "see https://github.com/golang/tools/blob/master/gopls/doc/workspace.md " + + "for information on setting up multi-module workspaces using go.work files" + result.softErrorf(msg) result.setBool(&o.ExperimentalWorkspaceModule) case "experimentalTemplateSupport": // TODO(pjw): remove after June 2022 @@ -993,14 +1104,18 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) o.TemplateExtensions = nil break } - result.errorf(fmt.Sprintf("unexpected type %T not []string", value)) - case "experimentalDiagnosticsDelay", "diagnosticsDelay": - if name == "experimentalDiagnosticsDelay" { - result.deprecated("diagnosticsDelay") - } + result.parseErrorf("unexpected type %T not []string", value) + + case "experimentalDiagnosticsDelay": + result.deprecated("diagnosticsDelay") + + case "diagnosticsDelay": result.setDuration(&o.DiagnosticsDelay) case "experimentalWatchedFileDelay": + const msg = "experimentalWatchedFileDelay is deprecated, and will " + + "be removed in a future version of gopls (https://go.dev/issue/55332)" + result.softErrorf(msg) result.setDuration(&o.ExperimentalWatchedFileDelay) case "experimentalPackageCacheKey": @@ -1013,12 +1128,24 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.AllowImplicitNetworkAccess) case "experimentalUseInvalidMetadata": + const msg = "experimentalUseInvalidMetadata is deprecated, and will be removed " + + "in a future version of gopls (https://go.dev/issue/55333)" + result.softErrorf(msg) result.setBool(&o.ExperimentalUseInvalidMetadata) + case "standaloneTags": + result.setStringSlice(&o.StandaloneTags) + case "allExperiments": // This setting should be handled before all of the other options are // processed, so do nothing here. + case "newDiff": + result.setString(&o.NewDiff) + + case "chattyDiagnostics": + result.setBool(&o.ChattyDiagnostics) + // Replaced settings. case "experimentalDisabledAnalyses": result.deprecated("analyses") @@ -1060,7 +1187,11 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) return result } -func (r *OptionResult) errorf(msg string, values ...interface{}) { +// parseErrorf reports an error parsing the current configuration value. +func (r *OptionResult) parseErrorf(msg string, values ...interface{}) { + if false { + _ = fmt.Sprintf(msg, values...) // this causes vet to check this like printf + } prefix := fmt.Sprintf("parsing setting %q: ", r.Name) r.Error = fmt.Errorf(prefix+msg, values...) } @@ -1074,6 +1205,16 @@ func (e *SoftError) Error() string { return e.msg } +// softErrorf reports an error that does not affect the functionality of gopls +// (a warning in the UI). +// The formatted message will be shown to the user unmodified. +func (r *OptionResult) softErrorf(format string, values ...interface{}) { + msg := fmt.Sprintf(format, values...) + r.Error = &SoftError{msg} +} + +// deprecated reports the current setting as deprecated. If 'replacement' is +// non-nil, it is suggested to the user. func (r *OptionResult) deprecated(replacement string) { msg := fmt.Sprintf("gopls setting %q is deprecated", r.Name) if replacement != "" { @@ -1082,6 +1223,7 @@ func (r *OptionResult) deprecated(replacement string) { r.Error = &SoftError{msg} } +// unexpected reports that the current setting is not known to gopls. func (r *OptionResult) unexpected() { r.Error = fmt.Errorf("unexpected gopls setting %q", r.Name) } @@ -1089,7 +1231,7 @@ func (r *OptionResult) unexpected() { func (r *OptionResult) asBool() (bool, bool) { b, ok := r.Value.(bool) if !ok { - r.errorf("invalid type %T, expect bool", r.Value) + r.parseErrorf("invalid type %T, expect bool", r.Value) return false, false } return b, true @@ -1105,7 +1247,7 @@ func (r *OptionResult) setDuration(d *time.Duration) { if v, ok := r.asString(); ok { parsed, err := time.ParseDuration(v) if err != nil { - r.errorf("failed to parse duration %q: %v", v, err) + r.parseErrorf("failed to parse duration %q: %v", v, err) return } *d = parsed @@ -1137,18 +1279,18 @@ func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) { switch k { case "noEscape": m[Escape] = false - r.errorf(`"noEscape" is deprecated, set "Escape: false" instead`) + r.parseErrorf(`"noEscape" is deprecated, set "Escape: false" instead`) case "noNilcheck": m[Nil] = false - r.errorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) + r.parseErrorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) case "noInline": m[Inline] = false - r.errorf(`"noInline" is deprecated, set "Inline: false" instead`) + r.parseErrorf(`"noInline" is deprecated, set "Inline: false" instead`) case "noBounds": m[Bounds] = false - r.errorf(`"noBounds" is deprecated, set "Bounds: false" instead`) + r.parseErrorf(`"noBounds" is deprecated, set "Bounds: false" instead`) default: - r.errorf(err.Error()) + r.parseErrorf("%v", err) } continue } @@ -1160,15 +1302,15 @@ func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) { func (r *OptionResult) asBoolMap() map[string]bool { all, ok := r.Value.(map[string]interface{}) if !ok { - r.errorf("invalid type %T for map[string]bool option", r.Value) + r.parseErrorf("invalid type %T for map[string]bool option", r.Value) return nil } m := make(map[string]bool) for a, enabled := range all { - if enabled, ok := enabled.(bool); ok { - m[a] = enabled + if e, ok := enabled.(bool); ok { + m[a] = e } else { - r.errorf("invalid type %T for map key %q", enabled, a) + r.parseErrorf("invalid type %T for map key %q", enabled, a) return m } } @@ -1178,12 +1320,30 @@ func (r *OptionResult) asBoolMap() map[string]bool { func (r *OptionResult) asString() (string, bool) { b, ok := r.Value.(string) if !ok { - r.errorf("invalid type %T, expect string", r.Value) + r.parseErrorf("invalid type %T, expect string", r.Value) return "", false } return b, true } +func (r *OptionResult) asStringSlice() ([]string, bool) { + iList, ok := r.Value.([]interface{}) + if !ok { + r.parseErrorf("invalid type %T, expect list", r.Value) + return nil, false + } + var list []string + for _, elem := range iList { + s, ok := elem.(string) + if !ok { + r.parseErrorf("invalid element type %T, expect string", elem) + return nil, false + } + list = append(list, s) + } + return list, true +} + func (r *OptionResult) asOneOf(options ...string) (string, bool) { s, ok := r.asString() if !ok { @@ -1191,7 +1351,7 @@ func (r *OptionResult) asOneOf(options ...string) (string, bool) { } s, err := asOneOf(s, options...) if err != nil { - r.errorf(err.Error()) + r.parseErrorf("%v", err) } return s, err == nil } @@ -1212,6 +1372,12 @@ func (r *OptionResult) setString(s *string) { } } +func (r *OptionResult) setStringSlice(s *[]string) { + if v, ok := r.asStringSlice(); ok { + *s = v + } +} + // EnabledAnalyzers returns all of the analyzers enabled for the given // snapshot. func EnabledAnalyzers(snapshot Snapshot) (analyzers []*Analyzer) { @@ -1258,6 +1424,10 @@ func typeErrorAnalyzers() map[string]*Analyzer { Fix: UndeclaredName, Enabled: true, }, + unusedvariable.Analyzer.Name: { + Analyzer: unusedvariable.Analyzer, + Enabled: false, + }, } } @@ -1319,6 +1489,7 @@ func defaultAnalyzers() map[string]*Analyzer { useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false}, infertypeargs.Analyzer.Name: {Analyzer: infertypeargs.Analyzer, Enabled: true}, embeddirective.Analyzer.Name: {Analyzer: embeddirective.Analyzer, Enabled: true}, + timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true}, // gofmt -s suite: simplifycompositelit.Analyzer.Name: { @@ -1351,6 +1522,7 @@ type APIJSON struct { Commands []*CommandJSON Lenses []*LensJSON Analyzers []*AnalyzerJSON + Hints []*HintJSON } type OptionJSON struct { @@ -1416,12 +1588,8 @@ func collectEnums(opt *OptionJSON) string { } func shouldShowEnumKeysInSettings(name string) bool { - // Both of these fields have too many possible options to print. - return !hardcodedEnumKeys(name) -} - -func hardcodedEnumKeys(name string) bool { - return name == "analyses" || name == "codelenses" + // These fields have too many possible options to print. + return !(name == "analyses" || name == "codelenses" || name == "hints") } type EnumKeys struct { @@ -1489,3 +1657,17 @@ func (a *AnalyzerJSON) String() string { func (a *AnalyzerJSON) Write(w io.Writer) { fmt.Fprintf(w, "%s (%s): %v", a.Name, a.Doc, a.Default) } + +type HintJSON struct { + Name string + Doc string + Default bool +} + +func (h *HintJSON) String() string { + return h.Name +} + +func (h *HintJSON) Write(w io.Writer) { + fmt.Fprintf(w, "%s (%s): %v", h.Name, h.Doc, h.Default) +} diff --git a/internal/lsp/source/options_test.go b/gopls/internal/lsp/source/options_test.go similarity index 100% rename from internal/lsp/source/options_test.go rename to gopls/internal/lsp/source/options_test.go diff --git a/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go similarity index 63% rename from internal/lsp/source/references.go rename to gopls/internal/lsp/source/references.go index 3541600b207..e26091c9fe8 100644 --- a/internal/lsp/source/references.go +++ b/gopls/internal/lsp/source/references.go @@ -9,13 +9,16 @@ import ( "errors" "fmt" "go/ast" + "go/token" "go/types" "sort" + "strconv" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" ) // ReferenceInfo holds information about reference to an identifier in Go source. @@ -28,12 +31,74 @@ type ReferenceInfo struct { isDeclaration bool } +// isInPackageName reports whether the file's package name surrounds the +// given position pp (e.g. "foo" surrounds the cursor in "package foo"). +func isInPackageName(ctx context.Context, s Snapshot, f FileHandle, pgf *ParsedGoFile, pp protocol.Position) (bool, error) { + // Find position of the package name declaration + cursorPos, err := pgf.Mapper.Pos(pp) + if err != nil { + return false, err + } + + return pgf.File.Name.Pos() <= cursorPos && cursorPos <= pgf.File.Name.End(), nil +} + // References returns a list of references for a given identifier within the packages // containing i.File. Declarations appear first in the result. func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]*ReferenceInfo, error) { ctx, done := event.Start(ctx, "source.References") defer done() + // Find position of the package name declaration + pgf, err := s.ParseGo(ctx, f, ParseFull) + if err != nil { + return nil, err + } + + packageName := pgf.File.Name.Name // from package decl + inPackageName, err := isInPackageName(ctx, s, f, pgf, pp) + if err != nil { + return nil, err + } + + if inPackageName { + // TODO(rfindley): this is inaccurate, excluding test variants, and + // redundant with package renaming. Refactor to share logic. + renamingPkg, err := s.PackageForFile(ctx, f.URI(), TypecheckWorkspace, NarrowestPackage) + if err != nil { + return nil, err + } + + // Find external references to the package. + rdeps, err := s.GetReverseDependencies(ctx, renamingPkg.ID()) + if err != nil { + return nil, err + } + var refs []*ReferenceInfo + for _, dep := range rdeps { + for _, f := range dep.CompiledGoFiles() { + for _, imp := range f.File.Imports { + if path, err := strconv.Unquote(imp.Path.Value); err == nil && path == renamingPkg.PkgPath() { + refs = append(refs, &ReferenceInfo{ + Name: packageName, + MappedRange: NewMappedRange(f.Tok, f.Mapper, imp.Pos(), imp.End()), + }) + } + } + } + } + + // Find internal references to the package within the package itself + for _, f := range renamingPkg.CompiledGoFiles() { + refs = append(refs, &ReferenceInfo{ + Name: packageName, + MappedRange: NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()), + }) + } + + return refs, nil + } + qualifiedObjs, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) // Don't return references for builtin types. if errors.Is(err, errBuiltin) { @@ -63,16 +128,22 @@ func References(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Posit } // references is a helper function to avoid recomputing qualifiedObjsAtProtocolPos. +// The first element of qos is considered to be the declaration; +// if isDeclaration, the first result is an extra item for it. +// Only the definition-related fields of qualifiedObject are used. +// (Arguably it should accept a smaller data type.) func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, includeDeclaration, includeInterfaceRefs, includeEmbeddedRefs bool) ([]*ReferenceInfo, error) { var ( references []*ReferenceInfo - seen = make(map[token.Pos]bool) + seen = make(map[positionKey]bool) ) pos := qos[0].obj.Pos() if pos == token.NoPos { - return nil, fmt.Errorf("no position for %s", qos[0].obj) + return nil, fmt.Errorf("no position for %s", qos[0].obj) // e.g. error.Error } + // Inv: qos[0].pkg != nil, since Pos is valid. + // Inv: qos[*].pkg != nil, since all qos are logically the same declaration. filename := snapshot.FileSet().Position(pos).Filename pgf, err := qos[0].pkg.File(span.URIFromPath(filename)) if err != nil { @@ -128,11 +199,16 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i continue } } - if seen[ident.Pos()] { + key, found := packagePositionKey(pkg, ident.Pos()) + if !found { + bug.Reportf("ident %v (pos: %v) not found in package %v", ident.Name, ident.Pos(), pkg.Name()) + continue + } + if seen[key] { continue } - seen[ident.Pos()] = true - rng, err := posToMappedRange(snapshot, pkg, ident.Pos(), ident.End()) + seen[key] = true + rng, err := posToMappedRange(snapshot.FileSet(), pkg, ident.Pos(), ident.End()) if err != nil { return nil, err } @@ -152,6 +228,8 @@ func references(ctx context.Context, snapshot Snapshot, qos []qualifiedObject, i // happened to have a String method. _, isType := declIdent.Declaration.obj.(*types.TypeName) if includeInterfaceRefs && !isType { + // TODO(adonovan): opt: don't go back into the position domain: + // we have complete type information already. declRange, err := declIdent.Range() if err != nil { return nil, err @@ -188,6 +266,8 @@ func interfaceReferences(ctx context.Context, s Snapshot, f FileHandle, pp proto return nil, err } + // Make a separate call to references() for each element + // since it treats the first qualifiedObject as a definition. var refs []*ReferenceInfo for _, impl := range implementations { implRefs, err := references(ctx, s, []qualifiedObject{impl}, false, false, false) diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go new file mode 100644 index 00000000000..2d4a188b8ac --- /dev/null +++ b/gopls/internal/lsp/source/rename.go @@ -0,0 +1,715 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "errors" + "fmt" + "go/ast" + "go/token" + "go/types" + "path" + "regexp" + "sort" + "strconv" + "strings" + + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/refactor/satisfy" +) + +type renamer struct { + ctx context.Context + fset *token.FileSet + refs []*ReferenceInfo + objsToUpdate map[types.Object]bool + hadConflicts bool + errors string + from, to string + satisfyConstraints map[satisfy.Constraint]bool + packages map[*types.Package]Package // may include additional packages that are a dep of pkg. + msets typeutil.MethodSetCache + changeMethods bool +} + +type PrepareItem struct { + Range protocol.Range + Text string +} + +// PrepareRename searches for a valid renaming at position pp. +// +// The returned usererr is intended to be displayed to the user to explain why +// the prepare fails. Probably we could eliminate the redundancy in returning +// two errors, but for now this is done defensively. +func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) { + // Find position of the package name declaration. + ctx, done := event.Start(ctx, "source.PrepareRename") + defer done() + pgf, err := snapshot.ParseGo(ctx, f, ParseFull) + if err != nil { + return nil, err, err + } + inPackageName, err := isInPackageName(ctx, snapshot, f, pgf, pp) + if err != nil { + return nil, err, err + } + + if inPackageName { + fileRenameSupported := false + for _, op := range snapshot.View().Options().SupportedResourceOperations { + if op == protocol.Rename { + fileRenameSupported = true + break + } + } + + if !fileRenameSupported { + err := errors.New("can't rename package: LSP client does not support file renaming") + return nil, err, err + } + fileMeta, err := snapshot.MetadataForFile(ctx, f.URI()) + if err != nil { + return nil, err, err + } + + if len(fileMeta) == 0 { + err := fmt.Errorf("no packages found for file %q", f.URI()) + return nil, err, err + } + + meta := fileMeta[0] + + if meta.PackageName() == "main" { + err := errors.New("can't rename package \"main\"") + return nil, err, err + } + + if strings.HasSuffix(meta.PackageName(), "_test") { + err := errors.New("can't rename x_test packages") + return nil, err, err + } + + if meta.ModuleInfo() == nil { + err := fmt.Errorf("can't rename package: missing module information for package %q", meta.PackagePath()) + return nil, err, err + } + + if meta.ModuleInfo().Path == meta.PackagePath() { + err := fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PackagePath(), meta.ModuleInfo().Path) + return nil, err, err + } + // TODO(rfindley): we should not need the package here. + pkg, err := snapshot.WorkspacePackageByID(ctx, meta.PackageID()) + if err != nil { + err = fmt.Errorf("error building package to rename: %v", err) + return nil, err, err + } + result, err := computePrepareRenameResp(snapshot, pkg, pgf.File.Name, pkg.Name()) + if err != nil { + return nil, nil, err + } + return result, nil, nil + } + + qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp) + if err != nil { + return nil, nil, err + } + node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg + if err := checkRenamable(obj); err != nil { + return nil, nil, err + } + result, err := computePrepareRenameResp(snapshot, pkg, node, obj.Name()) + if err != nil { + return nil, nil, err + } + return result, nil, nil +} + +func computePrepareRenameResp(snapshot Snapshot, pkg Package, node ast.Node, text string) (*PrepareItem, error) { + mr, err := posToMappedRange(snapshot.FileSet(), pkg, node.Pos(), node.End()) + if err != nil { + return nil, err + } + rng, err := mr.Range() + if err != nil { + return nil, err + } + if _, isImport := node.(*ast.ImportSpec); isImport { + // We're not really renaming the import path. + rng.End = rng.Start + } + return &PrepareItem{ + Range: rng, + Text: text, + }, nil +} + +// checkRenamable verifies if an obj may be renamed. +func checkRenamable(obj types.Object) error { + if v, ok := obj.(*types.Var); ok && v.Embedded() { + return errors.New("can't rename embedded fields: rename the type directly or name the field") + } + if obj.Name() == "_" { + return errors.New("can't rename \"_\"") + } + return nil +} + +// Rename returns a map of TextEdits for each file modified when renaming a +// given identifier within a package and a boolean value of true for renaming +// package and false otherwise. +func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, bool, error) { + ctx, done := event.Start(ctx, "source.Rename") + defer done() + + pgf, err := s.ParseGo(ctx, f, ParseFull) + if err != nil { + return nil, false, err + } + inPackageName, err := isInPackageName(ctx, s, f, pgf, pp) + if err != nil { + return nil, false, err + } + + if inPackageName { + if !isValidIdentifier(newName) { + return nil, true, fmt.Errorf("%q is not a valid identifier", newName) + } + + fileMeta, err := s.MetadataForFile(ctx, f.URI()) + if err != nil { + return nil, true, err + } + + if len(fileMeta) == 0 { + return nil, true, fmt.Errorf("no packages found for file %q", f.URI()) + } + + // We need metadata for the relevant package and module paths. These should + // be the same for all packages containing the file. + // + // TODO(rfindley): we mix package path and import path here haphazardly. + // Fix this. + meta := fileMeta[0] + oldPath := meta.PackagePath() + var modulePath string + if mi := meta.ModuleInfo(); mi == nil { + return nil, true, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PackagePath()) + } else { + modulePath = mi.Path + } + + if strings.HasSuffix(newName, "_test") { + return nil, true, fmt.Errorf("cannot rename to _test package") + } + + metadata, err := s.AllValidMetadata(ctx) + if err != nil { + return nil, true, err + } + + renamingEdits, err := renamePackage(ctx, s, modulePath, oldPath, newName, metadata) + if err != nil { + return nil, true, err + } + + return renamingEdits, true, nil + } + + qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) + if err != nil { + return nil, false, err + } + result, err := renameObj(ctx, s, newName, qos) + if err != nil { + return nil, false, err + } + + return result, false, nil +} + +// renamePackage computes all workspace edits required to rename the package +// described by the given metadata, to newName, by renaming its package +// directory. +// +// It updates package clauses and import paths for the renamed package as well +// as any other packages affected by the directory renaming among packages +// described by allMetadata. +func renamePackage(ctx context.Context, s Snapshot, modulePath, oldPath, newName string, allMetadata []Metadata) (map[span.URI][]protocol.TextEdit, error) { + if modulePath == oldPath { + return nil, fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath) + } + + newPathPrefix := path.Join(path.Dir(oldPath), newName) + + edits := make(map[span.URI][]protocol.TextEdit) + seen := make(seenPackageRename) // track per-file import renaming we've already processed + + // Rename imports to the renamed package from other packages. + for _, m := range allMetadata { + // Special case: x_test packages for the renamed package will not have the + // package path as as a dir prefix, but still need their package clauses + // renamed. + if m.PackagePath() == oldPath+"_test" { + newTestName := newName + "_test" + + if err := renamePackageClause(ctx, m, s, newTestName, seen, edits); err != nil { + return nil, err + } + continue + } + + // Subtle: check this condition before checking for valid module info + // below, because we should not fail this operation if unrelated packages + // lack module info. + if !strings.HasPrefix(m.PackagePath()+"/", oldPath+"/") { + continue // not affected by the package renaming + } + + if m.ModuleInfo() == nil { + return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PackagePath()) + } + + if modulePath != m.ModuleInfo().Path { + continue // don't edit imports if nested package and renaming package have different module paths + } + + // Renaming a package consists of changing its import path and package name. + suffix := strings.TrimPrefix(m.PackagePath(), oldPath) + newPath := newPathPrefix + suffix + + pkgName := m.PackageName() + if m.PackagePath() == oldPath { + pkgName = newName + + if err := renamePackageClause(ctx, m, s, newName, seen, edits); err != nil { + return nil, err + } + } + + if err := renameImports(ctx, s, m, newPath, pkgName, seen, edits); err != nil { + return nil, err + } + } + + return edits, nil +} + +// seenPackageRename tracks import path renamings that have already been +// processed. +// +// Due to test variants, files may appear multiple times in the reverse +// transitive closure of a renamed package, or in the reverse transitive +// closure of different variants of a renamed package (both are possible). +// However, in all cases the resulting edits will be the same. +type seenPackageRename map[seenPackageKey]bool +type seenPackageKey struct { + uri span.URI + importPath string +} + +// add reports whether uri and importPath have been seen, and records them as +// seen if not. +func (s seenPackageRename) add(uri span.URI, importPath string) bool { + key := seenPackageKey{uri, importPath} + seen := s[key] + if !seen { + s[key] = true + } + return seen +} + +// renamePackageClause computes edits renaming the package clause of files in +// the package described by the given metadata, to newName. +// +// As files may belong to multiple packages, the seen map tracks files whose +// package clause has already been updated, to prevent duplicate edits. +// +// Edits are written into the edits map. +func renamePackageClause(ctx context.Context, m Metadata, s Snapshot, newName string, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { + pkg, err := s.WorkspacePackageByID(ctx, m.PackageID()) + if err != nil { + return err + } + + // Rename internal references to the package in the renaming package. + for _, f := range pkg.CompiledGoFiles() { + if seen.add(f.URI, m.PackagePath()) { + continue + } + + if f.File.Name == nil { + continue + } + pkgNameMappedRange := NewMappedRange(f.Tok, f.Mapper, f.File.Name.Pos(), f.File.Name.End()) + rng, err := pkgNameMappedRange.Range() + if err != nil { + return err + } + edits[f.URI] = append(edits[f.URI], protocol.TextEdit{ + Range: rng, + NewText: newName, + }) + } + + return nil +} + +// renameImports computes the set of edits to imports resulting from renaming +// the package described by the given metadata, to a package with import path +// newPath and name newName. +// +// Edits are written into the edits map. +func renameImports(ctx context.Context, s Snapshot, m Metadata, newPath, newName string, seen seenPackageRename, edits map[span.URI][]protocol.TextEdit) error { + // TODO(rfindley): we should get reverse dependencies as metadata first, + // rather then building the package immediately. We don't need reverse + // dependencies if they are intermediate test variants. + rdeps, err := s.GetReverseDependencies(ctx, m.PackageID()) + if err != nil { + return err + } + + for _, dep := range rdeps { + // Subtle: don't perform renaming in this package if it is not fully + // parsed. This can occur inside the workspace if dep is an intermediate + // test variant. ITVs are only ever parsed in export mode, and no file is + // found only in an ITV. Therefore the renaming will eventually occur in a + // full package. + // + // An alternative algorithm that may be more robust would be to first + // collect *files* that need to have their imports updated, and then + // perform the rename using s.PackageForFile(..., NarrowestPackage). + if dep.ParseMode() != ParseFull { + continue + } + + for _, f := range dep.CompiledGoFiles() { + if seen.add(f.URI, m.PackagePath()) { + continue + } + + for _, imp := range f.File.Imports { + if impPath, _ := strconv.Unquote(imp.Path.Value); impPath != m.PackagePath() { + continue // not the import we're looking for + } + + // Create text edit for the import path (string literal). + impPathMappedRange := NewMappedRange(f.Tok, f.Mapper, imp.Path.Pos(), imp.Path.End()) + rng, err := impPathMappedRange.Range() + if err != nil { + return err + } + newText := strconv.Quote(newPath) + edits[f.URI] = append(edits[f.URI], protocol.TextEdit{ + Range: rng, + NewText: newText, + }) + + // If the package name of an import has not changed or if its import + // path already has a local package name, then we don't need to update + // the local package name. + if newName == m.PackageName() || imp.Name != nil { + continue + } + + // Rename the types.PkgName locally within this file. + pkgname := dep.GetTypesInfo().Implicits[imp].(*types.PkgName) + qos := []qualifiedObject{{obj: pkgname, pkg: dep}} + + pkgScope := dep.GetTypes().Scope() + fileScope := dep.GetTypesInfo().Scopes[f.File] + + var changes map[span.URI][]protocol.TextEdit + localName := newName + try := 0 + + // Keep trying with fresh names until one succeeds. + for fileScope.Lookup(localName) != nil || pkgScope.Lookup(localName) != nil { + try++ + localName = fmt.Sprintf("%s%d", newName, try) + } + changes, err = renameObj(ctx, s, localName, qos) + if err != nil { + return err + } + + // If the chosen local package name matches the package's new name, delete the + // change that would have inserted an explicit local name, which is always + // the lexically first change. + if localName == newName { + v := changes[f.URI] + sort.Slice(v, func(i, j int) bool { + return protocol.CompareRange(v[i].Range, v[j].Range) < 0 + }) + changes[f.URI] = v[1:] + } + for uri, changeEdits := range changes { + edits[uri] = append(edits[uri], changeEdits...) + } + } + } + } + + return nil +} + +// renameObj returns a map of TextEdits for renaming an identifier within a file +// and boolean value of true if there is no renaming conflicts and false otherwise. +func renameObj(ctx context.Context, s Snapshot, newName string, qos []qualifiedObject) (map[span.URI][]protocol.TextEdit, error) { + obj := qos[0].obj + + if err := checkRenamable(obj); err != nil { + return nil, err + } + if obj.Name() == newName { + return nil, fmt.Errorf("old and new names are the same: %s", newName) + } + if !isValidIdentifier(newName) { + return nil, fmt.Errorf("invalid identifier to rename: %q", newName) + } + refs, err := references(ctx, s, qos, true, false, true) + if err != nil { + return nil, err + } + r := renamer{ + ctx: ctx, + fset: s.FileSet(), + refs: refs, + objsToUpdate: make(map[types.Object]bool), + from: obj.Name(), + to: newName, + packages: make(map[*types.Package]Package), + } + + // A renaming initiated at an interface method indicates the + // intention to rename abstract and concrete methods as needed + // to preserve assignability. + for _, ref := range refs { + if obj, ok := ref.obj.(*types.Func); ok { + recv := obj.Type().(*types.Signature).Recv() + if recv != nil && IsInterface(recv.Type().Underlying()) { + r.changeMethods = true + break + } + } + } + for _, from := range refs { + r.packages[from.pkg.GetTypes()] = from.pkg + } + + // Check that the renaming of the identifier is ok. + for _, ref := range refs { + r.check(ref.obj) + if r.hadConflicts { // one error is enough. + break + } + } + if r.hadConflicts { + return nil, fmt.Errorf(r.errors) + } + + changes, err := r.update() + if err != nil { + return nil, err + } + + result := make(map[span.URI][]protocol.TextEdit) + for uri, edits := range changes { + // These edits should really be associated with FileHandles for maximal correctness. + // For now, this is good enough. + fh, err := s.GetFile(ctx, uri) + if err != nil { + return nil, err + } + data, err := fh.Read() + if err != nil { + return nil, err + } + m := protocol.NewColumnMapper(uri, data) + protocolEdits, err := ToProtocolEdits(m, edits) + if err != nil { + return nil, err + } + result[uri] = protocolEdits + } + return result, nil +} + +// Rename all references to the identifier. +func (r *renamer) update() (map[span.URI][]diff.Edit, error) { + result := make(map[span.URI][]diff.Edit) + seen := make(map[span.Span]bool) + + docRegexp, err := regexp.Compile(`\b` + r.from + `\b`) + if err != nil { + return nil, err + } + for _, ref := range r.refs { + refSpan, err := ref.Span() + if err != nil { + return nil, err + } + if seen[refSpan] { + continue + } + seen[refSpan] = true + + // Renaming a types.PkgName may result in the addition or removal of an identifier, + // so we deal with this separately. + if pkgName, ok := ref.obj.(*types.PkgName); ok && ref.isDeclaration { + edit, err := r.updatePkgName(pkgName) + if err != nil { + return nil, err + } + result[refSpan.URI()] = append(result[refSpan.URI()], *edit) + continue + } + + // Replace the identifier with r.to. + edit := diff.Edit{ + Start: refSpan.Start().Offset(), + End: refSpan.End().Offset(), + New: r.to, + } + + result[refSpan.URI()] = append(result[refSpan.URI()], edit) + + if !ref.isDeclaration || ref.ident == nil { // uses do not have doc comments to update. + continue + } + + doc := r.docComment(ref.pkg, ref.ident) + if doc == nil { + continue + } + + // Perform the rename in doc comments declared in the original package. + // go/parser strips out \r\n returns from the comment text, so go + // line-by-line through the comment text to get the correct positions. + for _, comment := range doc.List { + if isDirective(comment.Text) { + continue + } + // TODO(adonovan): why are we looping over lines? + // Just run the loop body once over the entire multiline comment. + lines := strings.Split(comment.Text, "\n") + tokFile := r.fset.File(comment.Pos()) + commentLine := tokFile.Line(comment.Pos()) + uri := span.URIFromPath(tokFile.Name()) + for i, line := range lines { + lineStart := comment.Pos() + if i > 0 { + lineStart = tokFile.LineStart(commentLine + i) + } + for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) { + // The File.Offset static check complains + // even though these uses are manifestly safe. + start, _ := safetoken.Offset(tokFile, lineStart+token.Pos(locs[0])) + end, _ := safetoken.Offset(tokFile, lineStart+token.Pos(locs[1])) + result[uri] = append(result[uri], diff.Edit{ + Start: start, + End: end, + New: r.to, + }) + } + } + } + } + + return result, nil +} + +// docComment returns the doc for an identifier. +func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { + _, tokFile, nodes, _ := pathEnclosingInterval(r.fset, pkg, id.Pos(), id.End()) + for _, node := range nodes { + switch decl := node.(type) { + case *ast.FuncDecl: + return decl.Doc + case *ast.Field: + return decl.Doc + case *ast.GenDecl: + return decl.Doc + // For {Type,Value}Spec, if the doc on the spec is absent, + // search for the enclosing GenDecl + case *ast.TypeSpec: + if decl.Doc != nil { + return decl.Doc + } + case *ast.ValueSpec: + if decl.Doc != nil { + return decl.Doc + } + case *ast.Ident: + case *ast.AssignStmt: + // *ast.AssignStmt doesn't have an associated comment group. + // So, we try to find a comment just before the identifier. + + // Try to find a comment group only for short variable declarations (:=). + if decl.Tok != token.DEFINE { + return nil + } + + identLine := tokFile.Line(id.Pos()) + for _, comment := range nodes[len(nodes)-1].(*ast.File).Comments { + if comment.Pos() > id.Pos() { + // Comment is after the identifier. + continue + } + + lastCommentLine := tokFile.Line(comment.End()) + if lastCommentLine+1 == identLine { + return comment + } + } + default: + return nil + } + } + return nil +} + +// updatePkgName returns the updates to rename a pkgName in the import spec by +// only modifying the package name portion of the import declaration. +func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.Edit, error) { + // Modify ImportSpec syntax to add or remove the Name as needed. + pkg := r.packages[pkgName.Pkg()] + _, tokFile, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos()) + if len(path) < 2 { + return nil, fmt.Errorf("no path enclosing interval for %s", pkgName.Name()) + } + spec, ok := path[1].(*ast.ImportSpec) + if !ok { + return nil, fmt.Errorf("failed to update PkgName for %s", pkgName.Name()) + } + + newText := "" + if pkgName.Imported().Name() != r.to { + newText = r.to + " " + } + + // Replace the portion (possibly empty) of the spec before the path: + // local "path" or "path" + // -> <- -><- + rng := span.NewRange(tokFile, spec.Pos(), spec.Path.Pos()) + spn, err := rng.Span() + if err != nil { + return nil, err + } + + return &diff.Edit{ + Start: spn.Start().Offset(), + End: spn.End().Offset(), + New: newText, + }, nil +} diff --git a/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go similarity index 97% rename from internal/lsp/source/rename_check.go rename to gopls/internal/lsp/source/rename_check.go index b17f9b87067..c4d5709bf74 100644 --- a/internal/lsp/source/rename_check.go +++ b/gopls/internal/lsp/source/rename_check.go @@ -372,7 +372,7 @@ func (r *renamer) checkStructField(from *types.Var) { if !ok { return } - pkg, path, _ := pathEnclosingInterval(r.fset, fromPkg, from.Pos(), from.Pos()) + pkg, _, path, _ := pathEnclosingInterval(r.fset, fromPkg, from.Pos(), from.Pos()) if pkg == nil || path == nil { return } @@ -821,13 +821,13 @@ func someUse(info *types.Info, obj types.Object) *ast.Ident { return nil } -// pathEnclosingInterval returns the Package and ast.Node that +// pathEnclosingInterval returns the Package, token.File, and ast.Node that // contain source interval [start, end), and all the node's ancestors // up to the AST root. It searches all ast.Files of all packages. // exact is defined as for astutil.PathEnclosingInterval. // // The zero value is returned if not found. -func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Pos) (resPkg Package, path []ast.Node, exact bool) { +func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Pos) (resPkg Package, tokFile *token.File, path []ast.Node, exact bool) { pkgs := []Package{pkg} for _, f := range pkg.GetSyntax() { for _, imp := range f.Imports { @@ -838,37 +838,38 @@ func pathEnclosingInterval(fset *token.FileSet, pkg Package, start, end token.Po if err != nil { continue } - importPkg, err := pkg.GetImport(importPath) + imported, err := pkg.ResolveImportPath(importPath) if err != nil { - return nil, nil, false + return nil, nil, nil, false } - pkgs = append(pkgs, importPkg) + pkgs = append(pkgs, imported) } } for _, p := range pkgs { for _, f := range p.GetSyntax() { - if f.Pos() == token.NoPos { + if !f.Pos().IsValid() { // This can happen if the parser saw // too many errors and bailed out. // (Use parser.AllErrors to prevent that.) continue } - if !tokenFileContainsPos(fset.File(f.Pos()), start) { + tokFile := fset.File(f.Pos()) + if !tokenFileContainsPos(tokFile, start) { continue } if path, exact := astutil.PathEnclosingInterval(f, start, end); path != nil { - return pkg, path, exact + return pkg, tokFile, path, exact } } } - return nil, nil, false + return nil, nil, nil, false } // TODO(adonovan): make this a method: func (*token.File) Contains(token.Pos) func tokenFileContainsPos(tf *token.File, pos token.Pos) bool { p := int(pos) base := tf.Base() - return base <= p && p < base+tf.Size() + return base <= p && p <= base+tf.Size() } func objectKind(obj types.Object) string { diff --git a/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go similarity index 90% rename from internal/lsp/source/signature_help.go rename to gopls/internal/lsp/source/signature_help.go index 813f67e7b3b..68ac1beeb25 100644 --- a/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -12,8 +12,8 @@ import ( "go/types" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" ) func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.SignatureInformation, int, error) { @@ -94,24 +94,12 @@ FindCall: comment *ast.CommentGroup ) if obj != nil { - declPkg, err := FindPackageFromPos(ctx, snapshot, obj.Pos()) + declPkg, err := FindPackageFromPos(snapshot.FileSet(), pkg, obj.Pos()) if err != nil { return nil, 0, err } - node, err := snapshot.PosToDecl(ctx, declPkg, obj.Pos()) - if err != nil { - return nil, 0, err - } - rng, err := objToMappedRange(snapshot, pkg, obj) - if err != nil { - return nil, 0, err - } - decl := Declaration{ - obj: obj, - node: node, - } - decl.MappedRange = append(decl.MappedRange, rng) - d, err := FindHoverContext(ctx, snapshot, pkg, decl.obj, decl.node, nil) + node, _ := FindDeclAndField(declPkg.GetSyntax(), obj.Pos()) // may be nil + d, err := FindHoverContext(ctx, snapshot, pkg, obj, node, nil) if err != nil { return nil, 0, err } diff --git a/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go similarity index 81% rename from internal/lsp/source/source_test.go rename to gopls/internal/lsp/source/source_test.go index 426bffc97b5..d7dc77c6a1c 100644 --- a/internal/lsp/source/source_test.go +++ b/gopls/internal/lsp/source/source_test.go @@ -15,16 +15,15 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/fuzzy" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/source/completion" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/completion" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/testenv" ) @@ -49,16 +48,16 @@ type runner struct { func testSource(t *testing.T, datum *tests.Data) { ctx := tests.Context(t) - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) session := cache.NewSession(ctx) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) options.SetEnvSlice(datum.Config.Env) view, _, release, err := session.NewView(ctx, "source_test", span.URIFromPath(datum.Config.Dir), options) - release() if err != nil { t.Fatal(err) } + release() defer view.Shutdown(ctx) // Enable type error analyses for tests. @@ -156,16 +155,7 @@ func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnost if err != nil { t.Fatal(err) } - // A special case to test that there are no diagnostics for a file. - if len(want) == 1 && want[0].Source == "no_diagnostics" { - if len(got) != 0 { - t.Errorf("expected no diagnostics for %s, got %v", uri, got) - } - return - } - if diff := tests.DiffDiagnostics(fileID.URI, want, got); diff != "" { - t.Error(diff) - } + tests.CompareDiagnostics(t, fileID.URI, want, got) } func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { @@ -373,11 +363,11 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, data s continue } tag := fmt.Sprintf("%s-%d", prefix, i) - want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) - if diff := tests.Diff(t, want, got); diff != "" { + if diff := compare.Text(want, got); diff != "" { t.Errorf("%s: foldingRanges failed for %s, diff:\n%v", tag, uri.Filename(), diff) } } @@ -400,11 +390,11 @@ func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, data s continue } tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i) - want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, tag, uri.Filename(), func() ([]byte, error) { return []byte(got), nil })) - if diff := tests.Diff(t, want, got); diff != "" { + if diff := compare.Text(want, got); diff != "" { t.Errorf("%s: failed for %s, diff:\n%v", tag, uri.Filename(), diff) } } @@ -470,7 +460,7 @@ func foldRanges(contents string, ranges []*source.FoldingRangeInfo) (string, err } func (r *runner) Format(t *testing.T, spn span.Span) { - gofmted := string(r.data.Golden("gofmt", spn.URI().Filename(), func() ([]byte, error) { + gofmted := string(r.data.Golden(t, "gofmt", spn.URI().Filename(), func() ([]byte, error) { cmd := exec.Command("gofmt", spn.URI().Filename()) out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files return out, nil @@ -486,19 +476,14 @@ func (r *runner) Format(t *testing.T, spn span.Span) { } return } - data, err := fh.Read() - if err != nil { - t.Fatal(err) - } m, err := r.data.Mapper(spn.URI()) if err != nil { t.Fatal(err) } - diffEdits, err := source.FromProtocolEdits(m, edits) + got, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Error(err) } - got := diff.ApplyEdits(string(data), diffEdits) if gofmted != got { t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", spn.URI().Filename(), gofmted, got) } @@ -517,28 +502,19 @@ func (r *runner) Import(t *testing.T, spn span.Span) { if err != nil { t.Error(err) } - data, err := fh.Read() - if err != nil { - t.Fatal(err) - } m, err := r.data.Mapper(fh.URI()) if err != nil { t.Fatal(err) } - diffEdits, err := source.FromProtocolEdits(m, edits) + got, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Error(err) } - got := diff.ApplyEdits(string(data), diffEdits) - want := string(r.data.Golden("goimports", spn.URI().Filename(), func() ([]byte, error) { + want := string(r.data.Golden(t, "goimports", spn.URI().Filename(), func() ([]byte, error) { return []byte(got), nil })) - if want != got { - d, err := myers.ComputeEdits(spn.URI(), want, got) - if err != nil { - t.Fatal(err) - } - t.Errorf("import failed for %s: %s", spn.URI().Filename(), diff.ToUnified("want", "got", want, d)) + if d := compare.Text(got, want); d != "" { + t.Errorf("import failed for %s:\n%s", spn.URI().Filename(), d) } } @@ -578,13 +554,14 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) { if hover != "" { didSomething = true tag := fmt.Sprintf("%s-hoverdef", d.Name) - expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) { + expectHover := string(r.data.Golden(t, tag, d.Src.URI().Filename(), func() ([]byte, error) { return []byte(hover), nil })) hover = tests.StripSubscripts(hover) expectHover = tests.StripSubscripts(expectHover) if hover != expectHover { - t.Errorf("hoverdef for %s failed:\n%s", d.Src, tests.Diff(t, expectHover, hover)) + tests.CheckSameMarkdown(t, hover, expectHover) + } } if !d.OnlyHover { @@ -685,6 +662,10 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) { } } +func (r *runner) InlayHints(t *testing.T, src span.Span) { + // TODO(golang/go#53315): add source test +} + func (r *runner) Hover(t *testing.T, src span.Span, text string) { ctx := r.ctx _, srcRng, err := spanToRange(r.data, src) @@ -772,9 +753,9 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { if err != nil { t.Fatal(err) } - changes, err := source.Rename(r.ctx, r.snapshot, fh, srcRng.Start, newText) + changes, _, err := source.Rename(r.ctx, r.snapshot, fh, srcRng.Start, newText) if err != nil { - renamed := string(r.data.Golden(tag, spn.URI().Filename(), func() ([]byte, error) { + renamed := string(r.data.Golden(t, tag, spn.URI().Filename(), func() ([]byte, error) { return []byte(err.Error()), nil })) if err.Error() != renamed { @@ -789,19 +770,14 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { if err != nil { t.Fatal(err) } - data, err := fh.Read() - if err != nil { - t.Fatal(err) - } m, err := r.data.Mapper(fh.URI()) if err != nil { t.Fatal(err) } - diffEdits, err := source.FromProtocolEdits(m, edits) + contents, _, err := source.ApplyProtocolEdits(m, edits) if err != nil { t.Fatal(err) } - contents := applyEdits(string(data), diffEdits) if len(changes) > 1 { filename := filepath.Base(editURI.Filename()) contents = fmt.Sprintf("%s:\n%s", filename, contents) @@ -820,7 +796,7 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { got += val } - renamed := string(r.data.Golden(tag, spn.URI().Filename(), func() ([]byte, error) { + renamed := string(r.data.Golden(t, tag, spn.URI().Filename(), func() ([]byte, error) { return []byte(got), nil })) @@ -829,102 +805,16 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { } } -func applyEdits(contents string, edits []diff.TextEdit) string { - res := contents - - // Apply the edits from the end of the file forward - // to preserve the offsets - for i := len(edits) - 1; i >= 0; i-- { - edit := edits[i] - start := edit.Span.Start().Offset() - end := edit.Span.End().Offset() - tmp := res[0:start] + edit.NewText - res = tmp + res[end:] - } - return res -} - func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { - _, srcRng, err := spanToRange(r.data, src) - if err != nil { - t.Fatal(err) - } - // Find the identifier at the position. - fh, err := r.snapshot.GetFile(r.ctx, src.URI()) - if err != nil { - t.Fatal(err) - } - item, _, err := source.PrepareRename(r.ctx, r.snapshot, fh, srcRng.Start) - if err != nil { - if want.Text != "" { // expected an ident. - t.Errorf("prepare rename failed for %v: got error: %v", src, err) - } - return - } - if item == nil { - if want.Text != "" { - t.Errorf("prepare rename failed for %v: got nil", src) - } - return - } - if want.Text == "" { - t.Errorf("prepare rename failed for %v: expected nil, got %v", src, item) - return - } - if item.Range.Start == item.Range.End { - // Special case for 0-length ranges. Marks can't specify a 0-length range, - // so just compare the start. - if item.Range.Start != want.Range.Start { - t.Errorf("prepare rename failed: incorrect point, got %v want %v", item.Range.Start, want.Range.Start) - } - } else { - if protocol.CompareRange(item.Range, want.Range) != 0 { - t.Errorf("prepare rename failed: incorrect range got %v want %v", item.Range, want.Range) - } - } + // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) { - fh, err := r.snapshot.GetFile(r.ctx, uri) - if err != nil { - t.Fatal(err) - } - symbols, err := source.DocumentSymbols(r.ctx, r.snapshot, fh) - if err != nil { - t.Errorf("symbols failed for %s: %v", uri, err) - } - if len(symbols) != len(expectedSymbols) { - t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols)) - return - } - if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" { - t.Error(diff) - } + // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } func (r *runner) WorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - r.callWorkspaceSymbols(t, uri, query, typ) -} - -func (r *runner) callWorkspaceSymbols(t *testing.T, uri span.URI, query string, typ tests.WorkspaceSymbolsTestType) { - t.Helper() - - matcher := tests.WorkspaceSymbolsTestTypeToMatcher(typ) - gotSymbols, err := source.WorkspaceSymbols(r.ctx, matcher, r.view.Options().SymbolStyle, []source.View{r.view}, query) - if err != nil { - t.Fatal(err) - } - got, err := tests.WorkspaceSymbolsString(r.ctx, r.data, uri, gotSymbols) - if err != nil { - t.Fatal(err) - } - got = filepath.ToSlash(tests.Normalize(got, r.normalizers)) - want := string(r.data.Golden(fmt.Sprintf("workspace_symbol-%s-%s", strings.ToLower(string(matcher)), query), uri.Filename(), func() ([]byte, error) { - return []byte(got), nil - })) - if diff := tests.Diff(t, want, got); diff != "" { - t.Error(diff) - } + // Removed in favor of just using the lsp_test implementation. See ../lsp_test.go } func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) { @@ -954,24 +844,18 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa Signatures: []protocol.SignatureInformation{*gotSignature}, ActiveParameter: uint32(gotActiveParameter), } - diff, err := tests.DiffSignatures(spn, want, got) - if err != nil { - t.Fatal(err) - } - if diff != "" { + if diff := tests.DiffSignatures(spn, want, got); diff != "" { t.Error(diff) } } // These are pure LSP features, no source level functionality to be tested. -func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {} - -func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string, expectedActions int) { -} -func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {} -func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) {} -func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {} -func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {} +func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) {} +func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actions []tests.SuggestedFix, want int) {} +func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {} +func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) {} +func (r *runner) CodeLens(t *testing.T, uri span.URI, want []protocol.CodeLens) {} +func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {} func spanToRange(data *tests.Data, spn span.Span) (*protocol.ColumnMapper, protocol.Range, error) { m, err := data.Mapper(spn.URI()) diff --git a/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go similarity index 94% rename from internal/lsp/source/stub.go rename to gopls/internal/lsp/source/stub.go index 0d1981795f2..28a2e2b23d3 100644 --- a/internal/lsp/source/stub.go +++ b/gopls/internal/lsp/source/stub.go @@ -17,10 +17,10 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/internal/lsp/analysis/stubmethods" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/safetoken" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/typeparams" ) @@ -80,20 +80,14 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi if err != nil { return nil, fmt.Errorf("format.Node: %w", err) } - diffEdits, err := snapshot.View().Options().ComputeEdits(parsedConcreteFile.URI, string(parsedConcreteFile.Src), source.String()) - if err != nil { - return nil, err - } + diffs := snapshot.View().Options().ComputeEdits(string(parsedConcreteFile.Src), source.String()) + tf := parsedConcreteFile.Mapper.TokFile var edits []analysis.TextEdit - for _, edit := range diffEdits { - rng, err := edit.Span.Range(parsedConcreteFile.Mapper.TokFile) - if err != nil { - return nil, err - } + for _, edit := range diffs { edits = append(edits, analysis.TextEdit{ - Pos: rng.Start, - End: rng.End, - NewText: []byte(edit.NewText), + Pos: tf.Pos(edit.Start), + End: tf.Pos(edit.End), + NewText: []byte(edit.New), }) } return &analysis.SuggestedFix{ @@ -261,8 +255,10 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method eiface := iface.Embedded(i).Obj() depPkg := ifacePkg if eiface.Pkg().Path() != ifacePkg.PkgPath() { + // TODO(adonovan): I'm not sure what this is trying to do, but it + // looks wrong the in case of type aliases. var err error - depPkg, err = ifacePkg.GetImport(eiface.Pkg().Path()) + depPkg, err = ifacePkg.DirectDep(eiface.Pkg().Path()) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/symbols.go b/gopls/internal/lsp/source/symbols.go new file mode 100644 index 00000000000..2bab241e1d5 --- /dev/null +++ b/gopls/internal/lsp/source/symbols.go @@ -0,0 +1,235 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "context" + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/gopls/internal/lsp/lsppos" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/event" +) + +func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.DocumentSymbol, error) { + ctx, done := event.Start(ctx, "source.DocumentSymbols") + defer done() + + content, err := fh.Read() + if err != nil { + return nil, err + } + + pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + if err != nil { + return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err) + } + + m := lsppos.NewTokenMapper(content, pgf.Tok) + + // Build symbols for file declarations. When encountering a declaration with + // errors (typically because positions are invalid), we skip the declaration + // entirely. VS Code fails to show any symbols if one of the top-level + // symbols is missing position information. + var symbols []protocol.DocumentSymbol + for _, decl := range pgf.File.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + if decl.Name.Name == "_" { + continue + } + fs, err := funcSymbol(m, decl) + if err == nil { + // If function is a method, prepend the type of the method. + if decl.Recv != nil && len(decl.Recv.List) > 0 { + fs.Name = fmt.Sprintf("(%s).%s", types.ExprString(decl.Recv.List[0].Type), fs.Name) + } + symbols = append(symbols, fs) + } + case *ast.GenDecl: + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.TypeSpec: + if spec.Name.Name == "_" { + continue + } + ts, err := typeSymbol(m, spec) + if err == nil { + symbols = append(symbols, ts) + } + case *ast.ValueSpec: + for _, name := range spec.Names { + if name.Name == "_" { + continue + } + vs, err := varSymbol(m, spec, name, decl.Tok == token.CONST) + if err == nil { + symbols = append(symbols, vs) + } + } + } + } + } + } + return symbols, nil +} + +func funcSymbol(m *lsppos.TokenMapper, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { + s := protocol.DocumentSymbol{ + Name: decl.Name.Name, + Kind: protocol.Function, + } + if decl.Recv != nil { + s.Kind = protocol.Method + } + var err error + s.Range, err = m.Range(decl.Pos(), decl.End()) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.SelectionRange, err = m.Range(decl.Name.Pos(), decl.Name.End()) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.Detail = types.ExprString(decl.Type) + return s, nil +} + +func typeSymbol(m *lsppos.TokenMapper, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { + s := protocol.DocumentSymbol{ + Name: spec.Name.Name, + } + var err error + s.Range, err = m.NodeRange(spec) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.SelectionRange, err = m.NodeRange(spec.Name) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.Kind, s.Detail, s.Children = typeDetails(m, spec.Type) + return s, nil +} + +func typeDetails(m *lsppos.TokenMapper, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { + switch typExpr := typExpr.(type) { + case *ast.StructType: + kind = protocol.Struct + children = fieldListSymbols(m, typExpr.Fields, protocol.Field) + if len(children) > 0 { + detail = "struct{...}" + } else { + detail = "struct{}" + } + + // Find interface methods and embedded types. + case *ast.InterfaceType: + kind = protocol.Interface + children = fieldListSymbols(m, typExpr.Methods, protocol.Method) + if len(children) > 0 { + detail = "interface{...}" + } else { + detail = "interface{}" + } + + case *ast.FuncType: + kind = protocol.Function + detail = types.ExprString(typExpr) + + default: + kind = protocol.Class // catch-all, for cases where we don't know the kind syntactically + detail = types.ExprString(typExpr) + } + return +} + +func fieldListSymbols(m *lsppos.TokenMapper, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { + if fields == nil { + return nil + } + + var symbols []protocol.DocumentSymbol + for _, field := range fields.List { + detail, children := "", []protocol.DocumentSymbol(nil) + if field.Type != nil { + _, detail, children = typeDetails(m, field.Type) + } + if len(field.Names) == 0 { // embedded interface or struct field + // By default, use the formatted type details as the name of this field. + // This handles potentially invalid syntax, as well as type embeddings in + // interfaces. + child := protocol.DocumentSymbol{ + Name: detail, + Kind: protocol.Field, // consider all embeddings to be fields + Children: children, + } + + // If the field is a valid embedding, promote the type name to field + // name. + selection := field.Type + if id := embeddedIdent(field.Type); id != nil { + child.Name = id.Name + child.Detail = detail + selection = id + } + + if rng, err := m.NodeRange(field.Type); err == nil { + child.Range = rng + } + if rng, err := m.NodeRange(selection); err == nil { + child.SelectionRange = rng + } + + symbols = append(symbols, child) + } else { + for _, name := range field.Names { + child := protocol.DocumentSymbol{ + Name: name.Name, + Kind: fieldKind, + Detail: detail, + Children: children, + } + + if rng, err := m.NodeRange(field); err == nil { + child.Range = rng + } + if rng, err := m.NodeRange(name); err == nil { + child.SelectionRange = rng + } + + symbols = append(symbols, child) + } + } + + } + return symbols +} + +func varSymbol(m *lsppos.TokenMapper, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { + s := protocol.DocumentSymbol{ + Name: name.Name, + Kind: protocol.Variable, + } + if isConst { + s.Kind = protocol.Constant + } + var err error + s.Range, err = m.NodeRange(spec) + if err != nil { + return protocol.DocumentSymbol{}, err + } + s.SelectionRange, err = m.NodeRange(name) + if err != nil { + return protocol.DocumentSymbol{}, err + } + if spec.Type != nil { // type may be missing from the syntax + _, s.Detail, s.Children = typeDetails(m, spec.Type) + } + return s, nil +} diff --git a/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go similarity index 93% rename from internal/lsp/source/types_format.go rename to gopls/internal/lsp/source/types_format.go index 93344e08678..f1c03cea658 100644 --- a/internal/lsp/source/types_format.go +++ b/gopls/internal/lsp/source/types_format.go @@ -15,9 +15,9 @@ import ( "go/types" "strings" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/typeparams" ) @@ -205,7 +205,7 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa params := make([]string, 0, sig.Params().Len()) for i := 0; i < sig.Params().Len(); i++ { el := sig.Params().At(i) - typ := FormatVarType(ctx, s, pkg, el, qf) + typ := FormatVarType(s.FileSet(), pkg, el, qf) p := typ if el.Name() != "" { p = el.Name() + " " + typ @@ -220,7 +220,7 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa needResultParens = true } el := sig.Results().At(i) - typ := FormatVarType(ctx, s, pkg, el, qf) + typ := FormatVarType(s.FileSet(), pkg, el, qf) if el.Name() == "" { results = append(results, typ) } else { @@ -253,16 +253,17 @@ func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signa // FormatVarType formats a *types.Var, accounting for type aliases. // To do this, it looks in the AST of the file in which the object is declared. // On any errors, it always falls back to types.TypeString. -func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj *types.Var, qf types.Qualifier) string { - pkg, err := FindPackageFromPos(ctx, snapshot, obj.Pos()) +func FormatVarType(fset *token.FileSet, srcpkg Package, obj *types.Var, qf types.Qualifier) string { + pkg, err := FindPackageFromPos(fset, srcpkg, obj.Pos()) if err != nil { return types.TypeString(obj.Type(), qf) } - expr, err := varType(ctx, snapshot, pkg, obj) - if err != nil { + _, field := FindDeclAndField(pkg.GetSyntax(), obj.Pos()) + if field == nil { return types.TypeString(obj.Type(), qf) } + expr := field.Type // If the given expr refers to a type parameter, then use the // object's Type instead of the type parameter declaration. This helps @@ -282,22 +283,10 @@ func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj * // If the request came from a different package than the one in which the // types are defined, we may need to modify the qualifiers. qualified = qualifyExpr(qualified, srcpkg, pkg, clonedInfo, qf) - fmted := FormatNode(snapshot.FileSet(), qualified) + fmted := FormatNode(fset, qualified) return fmted } -// varType returns the type expression for a *types.Var. -func varType(ctx context.Context, snapshot Snapshot, pkg Package, obj *types.Var) (ast.Expr, error) { - field, err := snapshot.PosToField(ctx, pkg, obj.Pos()) - if err != nil { - return nil, err - } - if field == nil { - return nil, fmt.Errorf("no declaration for object %s", obj.Name()) - } - return field.Type, nil -} - // qualifyExpr applies the "pkgName." prefix to any *ast.Ident in the expr. func qualifyExpr(expr ast.Expr, srcpkg, pkg Package, clonedInfo map[token.Pos]*types.PkgName, qf types.Qualifier) ast.Expr { ast.Inspect(expr, func(n ast.Node) bool { diff --git a/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go similarity index 73% rename from internal/lsp/source/util.go rename to gopls/internal/lsp/source/util.go index 9cb2ee69482..c933fbadd00 100644 --- a/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -17,39 +17,38 @@ import ( "strconv" "strings" - "golang.org/x/mod/modfile" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/typeparams" ) // MappedRange provides mapped protocol.Range for a span.Range, accounting for // UTF-16 code points. +// +// TOOD(adonovan): stop treating //line directives specially, and +// eliminate this type. All callers need either m, or a protocol.Range. type MappedRange struct { spanRange span.Range // the range in the compiled source (package.CompiledGoFiles) m *protocol.ColumnMapper // a mapper of the edited source (package.GoFiles) } -// NewMappedRange returns a MappedRange for the given start and end token.Pos. +// NewMappedRange returns a MappedRange for the given file and valid start/end token.Pos. // // By convention, start and end are assumed to be positions in the compiled (== // type checked) source, whereas the column mapper m maps positions in the -// user-edited source. Note that these may not be the same, as when using CGo: +// user-edited source. Note that these may not be the same, as when using goyacc or CGo: // CompiledGoFiles contains generated files, whose positions (via // token.File.Position) point to locations in the edited file -- the file // containing `import "C"`. -func NewMappedRange(fset *token.FileSet, m *protocol.ColumnMapper, start, end token.Pos) MappedRange { - if tf := fset.File(start); tf == nil { - bug.Report("nil file", nil) - } else { - mapped := m.TokFile.Name() - adjusted := tf.PositionFor(start, true) // adjusted position - if adjusted.Filename != mapped { - bug.Reportf("mapped file %q does not match start position file %q", mapped, adjusted.Filename) - } +func NewMappedRange(file *token.File, m *protocol.ColumnMapper, start, end token.Pos) MappedRange { + mapped := m.TokFile.Name() + adjusted := file.PositionFor(start, true) // adjusted position + if adjusted.Filename != mapped { + bug.Reportf("mapped file %q does not match start position file %q", mapped, adjusted.Filename) } return MappedRange{ - spanRange: span.NewRange(fset, start, end), + spanRange: span.NewRange(file, start, end), m: m, } } @@ -126,15 +125,8 @@ func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { return false } -func nodeToProtocolRange(snapshot Snapshot, pkg Package, n ast.Node) (protocol.Range, error) { - mrng, err := posToMappedRange(snapshot, pkg, n.Pos(), n.End()) - if err != nil { - return protocol.Range{}, err - } - return mrng.Range() -} - -func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedRange, error) { +func objToMappedRange(fset *token.FileSet, pkg Package, obj types.Object) (MappedRange, error) { + nameLen := len(obj.Name()) if pkgName, ok := obj.(*types.PkgName); ok { // An imported Go package has a package-local, unqualified name. // When the name matches the imported package name, there is no @@ -147,29 +139,58 @@ func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedR // When the identifier does not appear in the source, have the range // of the object be the import path, including quotes. if pkgName.Imported().Name() == pkgName.Name() { - return posToMappedRange(snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(len(pkgName.Imported().Path())+2)) + nameLen = len(pkgName.Imported().Path()) + len(`""`) } } - return nameToMappedRange(snapshot, pkg, obj.Pos(), obj.Name()) + return posToMappedRange(fset, pkg, obj.Pos(), obj.Pos()+token.Pos(nameLen)) } -func nameToMappedRange(snapshot Snapshot, pkg Package, pos token.Pos, name string) (MappedRange, error) { - return posToMappedRange(snapshot, pkg, pos, pos+token.Pos(len(name))) -} +// posToMappedRange returns the MappedRange for the given [start, end) span, +// which must be among the transitive dependencies of pkg. +func posToMappedRange(fset *token.FileSet, pkg Package, pos, end token.Pos) (MappedRange, error) { + if !pos.IsValid() { + return MappedRange{}, fmt.Errorf("invalid start position") + } + if !end.IsValid() { + return MappedRange{}, fmt.Errorf("invalid end position") + } -func posToMappedRange(snapshot Snapshot, pkg Package, pos, end token.Pos) (MappedRange, error) { - logicalFilename := snapshot.FileSet().File(pos).Position(pos).Filename + tokFile := fset.File(pos) + // Subtle: it is not safe to simplify this to tokFile.Name + // because, due to //line directives, a Position within a + // token.File may have a different filename than the File itself. + logicalFilename := tokFile.Position(pos).Filename pgf, _, err := findFileInDeps(pkg, span.URIFromPath(logicalFilename)) if err != nil { return MappedRange{}, err } + // It is problematic that pgf.Mapper (from the parsed Go file) is + // accompanied here not by pgf.Tok but by tokFile from the global + // FileSet, which is a distinct token.File that doesn't + // contain [pos,end). + // + // This is done because tokFile is the *token.File for the compiled go file + // containing pos, whereas Mapper is the UTF16 mapper for the go file pointed + // to by line directives. + // + // TODO(golang/go#55043): clean this up. + return NewMappedRange(tokFile, pgf.Mapper, pos, end), nil +} + +// FindPackageFromPos returns the Package for the given position, which must be +// among the transitive dependencies of pkg. +// +// TODO(rfindley): is this the best factoring of this API? This function is +// really a trivial wrapper around findFileInDeps, which may be a more useful +// function to expose. +func FindPackageFromPos(fset *token.FileSet, pkg Package, pos token.Pos) (Package, error) { if !pos.IsValid() { - return MappedRange{}, fmt.Errorf("invalid position for %v", pos) - } - if !end.IsValid() { - return MappedRange{}, fmt.Errorf("invalid position for %v", end) + return nil, fmt.Errorf("invalid position") } - return NewMappedRange(snapshot.FileSet(), pgf.Mapper, pos, end), nil + fileName := fset.File(pos).Name() + uri := span.URIFromPath(fileName) + _, pkg, err := findFileInDeps(pkg, uri) + return pkg, err } // Matches cgo generated comment as well as the proposed standard: @@ -290,35 +311,6 @@ func CompareDiagnostic(a, b *Diagnostic) int { return 0 } -// FindPackageFromPos finds the first package containing pos in its -// type-checked AST. -func FindPackageFromPos(ctx context.Context, snapshot Snapshot, pos token.Pos) (Package, error) { - tok := snapshot.FileSet().File(pos) - if tok == nil { - return nil, fmt.Errorf("no file for pos %v", pos) - } - uri := span.URIFromPath(tok.Name()) - pkgs, err := snapshot.PackagesForFile(ctx, uri, TypecheckAll, true) - if err != nil { - return nil, err - } - // Only return the package if it actually type-checked the given position. - for _, pkg := range pkgs { - parsed, err := pkg.File(uri) - if err != nil { - return nil, err - } - if parsed == nil { - continue - } - if parsed.Tok.Base() != tok.Base() { - continue - } - return pkg, nil - } - return nil, fmt.Errorf("no package for given file position") -} - // findFileInDeps finds uri in pkg or its dependencies. func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { queue := []Package{pkg} @@ -471,7 +463,7 @@ func CompareURI(left, right span.URI) int { // // Copied and slightly adjusted from go/src/cmd/go/internal/search/search.go. func InDir(dir, path string) bool { - if inDirLex(dir, path) { + if InDirLex(dir, path) { return true } if !honorSymlinks { @@ -481,18 +473,18 @@ func InDir(dir, path string) bool { if err != nil || xpath == path { xpath = "" } else { - if inDirLex(dir, xpath) { + if InDirLex(dir, xpath) { return true } } xdir, err := filepath.EvalSymlinks(dir) if err == nil && xdir != dir { - if inDirLex(xdir, path) { + if InDirLex(xdir, path) { return true } if xpath != "" { - if inDirLex(xdir, xpath) { + if InDirLex(xdir, xpath) { return true } } @@ -500,11 +492,11 @@ func InDir(dir, path string) bool { return false } -// inDirLex is like inDir but only checks the lexical form of the file names. +// InDirLex is like inDir but only checks the lexical form of the file names. // It does not consider symbolic links. // // Copied from go/src/cmd/go/internal/search/search.go. -func inDirLex(dir, path string) bool { +func InDirLex(dir, path string) bool { pv := strings.ToUpper(filepath.VolumeName(path)) dv := strings.ToUpper(filepath.VolumeName(dir)) path = path[len(pv):] @@ -545,9 +537,13 @@ func IsValidImport(pkgPath, importPkgPath string) bool { if i == -1 { return true } + // TODO(rfindley): this looks wrong: IsCommandLineArguments is meant to + // operate on package IDs, not package paths. if IsCommandLineArguments(string(pkgPath)) { return true } + // TODO(rfindley): this is wrong. mod.testx/p should not be able to + // import mod.test/internal: https://go.dev/play/p/-Ca6P-E4V4q return strings.HasPrefix(string(pkgPath), string(importPkgPath[:i])) } @@ -555,26 +551,56 @@ func IsValidImport(pkgPath, importPkgPath string) bool { // "command-line-arguments" package, which is a package with an unknown ID // created by the go command. It can have a test variant, which is why callers // should not check that a value equals "command-line-arguments" directly. +// +// TODO(rfindley): this should accept a PackageID. func IsCommandLineArguments(s string) bool { return strings.Contains(s, "command-line-arguments") } -// LineToRange creates a Range spanning start and end. -func LineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) { - return ByteOffsetsToRange(m, uri, start.Byte, end.Byte) +// RecvIdent returns the type identifier of a method receiver. +// e.g. A for all of A, *A, A[T], *A[T], etc. +func RecvIdent(recv *ast.FieldList) *ast.Ident { + if recv == nil || len(recv.List) == 0 { + return nil + } + x := recv.List[0].Type + if star, ok := x.(*ast.StarExpr); ok { + x = star.X + } + switch ix := x.(type) { // check for instantiated receivers + case *ast.IndexExpr: + x = ix.X + case *typeparams.IndexListExpr: + x = ix.X + } + if ident, ok := x.(*ast.Ident); ok { + return ident + } + return nil } -// ByteOffsetsToRange creates a range spanning start and end. -func ByteOffsetsToRange(m *protocol.ColumnMapper, uri span.URI, start, end int) (protocol.Range, error) { - line, col, err := span.ToPosition(m.TokFile, start) - if err != nil { - return protocol.Range{}, err - } - s := span.NewPoint(line, col, start) - line, col, err = span.ToPosition(m.TokFile, end) - if err != nil { - return protocol.Range{}, err +// embeddedIdent returns the type name identifier for an embedding x, if x in a +// valid embedding. Otherwise, it returns nil. +// +// Spec: An embedded field must be specified as a type name T or as a pointer +// to a non-interface type name *T +func embeddedIdent(x ast.Expr) *ast.Ident { + if star, ok := x.(*ast.StarExpr); ok { + x = star.X + } + switch ix := x.(type) { // check for instantiated receivers + case *ast.IndexExpr: + x = ix.X + case *typeparams.IndexListExpr: + x = ix.X + } + switch x := x.(type) { + case *ast.Ident: + return x + case *ast.SelectorExpr: + if _, ok := x.X.(*ast.Ident); ok { + return x.Sel + } } - e := span.NewPoint(line, col, end) - return m.Range(span.New(uri, s, e)) + return nil } diff --git a/internal/lsp/source/util_test.go b/gopls/internal/lsp/source/util_test.go similarity index 93% rename from internal/lsp/source/util_test.go rename to gopls/internal/lsp/source/util_test.go index 5d4e98f151c..60128e2344a 100644 --- a/internal/lsp/source/util_test.go +++ b/gopls/internal/lsp/source/util_test.go @@ -10,8 +10,8 @@ import ( "go/token" "testing" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" ) func TestMappedRangeAdjustment(t *testing.T) { @@ -41,7 +41,7 @@ const a𐐀b = 42`) start := cf.Pos(bytes.Index(compiled, []byte("a𐐀b"))) end := start + token.Pos(len("a𐐀b")) - mr := NewMappedRange(fset, mapper, start, end) + mr := NewMappedRange(cf, mapper, start, end) gotRange, err := mr.Range() if err != nil { t.Fatal(err) diff --git a/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go similarity index 81% rename from internal/lsp/source/view.go rename to gopls/internal/lsp/source/view.go index 94037f33fe3..25fa7d704d0 100644 --- a/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -7,6 +7,7 @@ package source import ( "bytes" "context" + "crypto/sha256" "errors" "fmt" "go/ast" @@ -20,11 +21,11 @@ import ( "golang.org/x/mod/module" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/govulncheck" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/lsp/progress" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" ) // Snapshot represents the current state for the given view. @@ -78,17 +79,6 @@ type Snapshot interface { // If the file is not available, returns nil and an error. ParseGo(ctx context.Context, fh FileHandle, mode ParseMode) (*ParsedGoFile, error) - // PosToField is a cache of *ast.Fields by token.Pos. This allows us - // to quickly find corresponding *ast.Field node given a *types.Var. - // We must refer to the AST to render type aliases properly when - // formatting signatures and other types. - PosToField(ctx context.Context, pkg Package, pos token.Pos) (*ast.Field, error) - - // PosToDecl maps certain objects' positions to their surrounding - // ast.Decl. This mapping is used when building the documentation - // string for the objects. - PosToDecl(ctx context.Context, pkg Package, pos token.Pos) (ast.Decl, error) - // DiagnosePackage returns basic diagnostics, including list, parse, and type errors // for pkg, grouped by file. DiagnosePackage(ctx context.Context, pkg Package) (map[span.URI][]*Diagnostic, error) @@ -146,9 +136,12 @@ type Snapshot interface { // IsBuiltin reports whether uri is part of the builtin package. IsBuiltin(ctx context.Context, uri span.URI) bool - // PackagesForFile returns the packages that this file belongs to, checked - // in mode. - PackagesForFile(ctx context.Context, uri span.URI, mode TypecheckMode, includeTestVariants bool) ([]Package, error) + // PackagesForFile returns an unordered list of packages that contain + // the file denoted by uri, type checked in the specified mode. + // + // If withIntermediateTestVariants is set, the resulting package set includes + // intermediate test variants. + PackagesForFile(ctx context.Context, uri span.URI, mode TypecheckMode, withIntermediateTestVariants bool) ([]Package, error) // PackageForFile returns a single package that this file belongs to, // checked in mode and filtered by the package policy. @@ -159,8 +152,8 @@ type Snapshot interface { GetReverseDependencies(ctx context.Context, id string) ([]Package, error) // CachedImportPaths returns all the imported packages loaded in this - // snapshot, indexed by their import path and checked in TypecheckWorkspace - // mode. + // snapshot, indexed by their package path (not import path, despite the name) + // and checked in TypecheckWorkspace mode. CachedImportPaths(ctx context.Context) (map[string]Package, error) // KnownPackages returns all the packages loaded in this snapshot, checked @@ -173,8 +166,15 @@ type Snapshot interface { // mode, this is just the reverse transitive closure of open packages. ActivePackages(ctx context.Context) ([]Package, error) + // AllValidMetadata returns all valid metadata loaded for the snapshot. + AllValidMetadata(ctx context.Context) ([]Metadata, error) + + // WorkspacePackageByID returns the workspace package with id, type checked + // in 'workspace' mode. + WorkspacePackageByID(ctx context.Context, id string) (Package, error) + // Symbols returns all symbols in the snapshot. - Symbols(ctx context.Context) (map[span.URI][]Symbol, error) + Symbols(ctx context.Context) map[span.URI][]Symbol // Metadata returns package metadata associated with the given file URI. MetadataForFile(ctx context.Context, uri span.URI) ([]Metadata, error) @@ -255,24 +255,40 @@ type View interface { // original one will be. SetOptions(context.Context, *Options) (View, error) - // Snapshot returns the current snapshot for the view. + // Snapshot returns the current snapshot for the view, and a + // release function that must be called when the Snapshot is + // no longer needed. Snapshot(ctx context.Context) (Snapshot, func()) - // Rebuild rebuilds the current view, replacing the original view in its session. - Rebuild(ctx context.Context) (Snapshot, func(), error) - // IsGoPrivatePath reports whether target is a private import path, as identified // by the GOPRIVATE environment variable. IsGoPrivatePath(path string) bool - // ModuleUpgrades returns known module upgrades. - ModuleUpgrades() map[string]string + // ModuleUpgrades returns known module upgrades for the dependencies of + // modfile. + ModuleUpgrades(modfile span.URI) map[string]string + + // RegisterModuleUpgrades registers that upgrades exist for the given modules + // required by modfile. + RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string) - // RegisterModuleUpgrades registers that upgrades exist for the given modules. - RegisterModuleUpgrades(upgrades map[string]string) + // ClearModuleUpgrades clears all upgrades for the modules in modfile. + ClearModuleUpgrades(modfile span.URI) + + // Vulnerabilites returns known vulnerabilities for the given modfile. + // TODO(suzmue): replace command.Vuln with a different type, maybe + // https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? + Vulnerabilities(modfile span.URI) []govulncheck.Vuln + + // SetVulnerabilities resets the list of vulnerabilites that exists for the given modules + // required by modfile. + SetVulnerabilities(modfile span.URI, vulnerabilities []govulncheck.Vuln) // FileKind returns the type of a file FileKind(FileHandle) FileKind + + // GoVersion returns the configured Go version for this view. + GoVersion() int } // A FileSource maps uris to FileHandles. This abstraction exists both for @@ -292,6 +308,7 @@ type ParsedGoFile struct { // Source code used to build the AST. It may be different from the // actual content of the file if we have fixed the AST. Src []byte + Fixed bool Mapper *protocol.ColumnMapper ParseErr scanner.ErrorList } @@ -321,7 +338,12 @@ type TidiedModule struct { } // Metadata represents package metadata retrieved from go/packages. +// +// TODO(rfindley): move the strongly typed strings from the cache package here. type Metadata interface { + // PackageID is the unique package id. + PackageID() string + // PackageName is the package name. PackageName() string @@ -332,65 +354,6 @@ type Metadata interface { ModuleInfo() *packages.Module } -// Session represents a single connection from a client. -// This is the level at which things like open files are maintained on behalf -// of the client. -// A session may have many active views at any given time. -type Session interface { - // ID returns the unique identifier for this session on this server. - ID() string - // NewView creates a new View, returning it and its first snapshot. If a - // non-empty tempWorkspace directory is provided, the View will record a copy - // of its gopls workspace module in that directory, so that client tooling - // can execute in the same main module. - NewView(ctx context.Context, name string, folder span.URI, options *Options) (View, Snapshot, func(), error) - - // Cache returns the cache that created this session, for debugging only. - Cache() interface{} - - // View returns a view with a matching name, if the session has one. - View(name string) View - - // ViewOf returns a view corresponding to the given URI. - ViewOf(uri span.URI) (View, error) - - // Views returns the set of active views built by this session. - Views() []View - - // Shutdown the session and all views it has created. - Shutdown(ctx context.Context) - - // GetFile returns a handle for the specified file. - GetFile(ctx context.Context, uri span.URI) (FileHandle, error) - - // DidModifyFile reports a file modification to the session. It returns - // the new snapshots after the modifications have been applied, paired with - // the affected file URIs for those snapshots. - DidModifyFiles(ctx context.Context, changes []FileModification) (map[Snapshot][]span.URI, []func(), error) - - // ExpandModificationsToDirectories returns the set of changes with the - // directory changes removed and expanded to include all of the files in - // the directory. - ExpandModificationsToDirectories(ctx context.Context, changes []FileModification) []FileModification - - // Overlays returns a slice of file overlays for the session. - Overlays() []Overlay - - // Options returns a copy of the SessionOptions for this session. - Options() *Options - - // SetOptions sets the options of this session to new values. - SetOptions(*Options) - - // FileWatchingGlobPatterns returns glob patterns to watch every directory - // known by the view. For views within a module, this is the module root, - // any directory in the module root, and any replace targets. - FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} - - // SetProgressTracker sets the progress tracker for the session. - SetProgressTracker(tracker *progress.Tracker) -} - var ErrViewExists = errors.New("view already exists for session") // Overlay is the type for a file held in memory on a session. @@ -477,21 +440,20 @@ const ( ParseFull ) +// AllParseModes contains all possible values of ParseMode. +// It is used for cache invalidation on a file content change. +var AllParseModes = []ParseMode{ParseHeader, ParseExported, ParseFull} + // TypecheckMode controls what kind of parsing should be done (see ParseMode) // while type checking a package. type TypecheckMode int const ( - // Invalid default value. - TypecheckUnknown TypecheckMode = iota // TypecheckFull means to use ParseFull. - TypecheckFull + TypecheckFull TypecheckMode = iota // TypecheckWorkspace means to use ParseFull for workspace packages, and // ParseExported for others. TypecheckWorkspace - // TypecheckAll means ParseFull for workspace packages, and both Full and - // Exported for others. Only valid for some functions. - TypecheckAll ) type VersionedFileHandle interface { @@ -528,12 +490,45 @@ type FileHandle interface { Saved() bool } +// A Hash is a cryptographic digest of the contents of a file. +// (Although at 32B it is larger than a 16B string header, it is smaller +// and has better locality than the string header + 64B of hex digits.) +type Hash [sha256.Size]byte + +// HashOf returns the hash of some data. +func HashOf(data []byte) Hash { + return Hash(sha256.Sum256(data)) +} + +// Hashf returns the hash of a printf-formatted string. +func Hashf(format string, args ...interface{}) Hash { + // Although this looks alloc-heavy, it is faster than using + // Fprintf on sha256.New() because the allocations don't escape. + return HashOf([]byte(fmt.Sprintf(format, args...))) +} + +// String returns the digest as a string of hex digits. +func (h Hash) String() string { + return fmt.Sprintf("%64x", [sha256.Size]byte(h)) +} + +// Less returns true if the given hash is less than the other. +func (h Hash) Less(other Hash) bool { + return bytes.Compare(h[:], other[:]) < 0 +} + +// XORWith updates *h to *h XOR h2. +func (h *Hash) XORWith(h2 Hash) { + // Small enough that we don't need crypto/subtle.XORBytes. + for i := range h { + h[i] ^= h2[i] + } +} + // FileIdentity uniquely identifies a file at a version from a FileSystem. type FileIdentity struct { - URI span.URI - - // Identifier represents a unique identifier for the file's content. - Hash string + URI span.URI + Hash Hash // digest of file contents } func (id FileIdentity) String() string { @@ -601,19 +596,19 @@ func (a Analyzer) IsEnabled(view View) bool { // Package represents a Go package that has been type-checked. It maintains // only the relevant fields of a *go/packages.Package. type Package interface { - ID() string - Name() string - PkgPath() string + ID() string // logically a cache.PackageID + Name() string // logically a cache.PackageName + PkgPath() string // logically a cache.PackagePath CompiledGoFiles() []*ParsedGoFile File(uri span.URI) (*ParsedGoFile, error) GetSyntax() []*ast.File GetTypes() *types.Package GetTypesInfo() *types.Info GetTypesSizes() types.Sizes - IsIllTyped() bool ForTest() string - GetImport(pkgPath string) (Package, error) - MissingDependencies() []string + DirectDep(packagePath string) (Package, error) // logically a cache.PackagePath + ResolveImportPath(importPath string) (Package, error) // logically a cache.ImportPath + MissingDependencies() []string // unordered; logically cache.ImportPaths Imports() []Package Version() *module.Version HasListOrParseErrors() bool @@ -621,11 +616,15 @@ type Package interface { ParseMode() ParseMode } +// A CriticalError is a workspace-wide error that generally prevents gopls from +// functioning correctly. In the presence of critical errors, other diagnostics +// in the workspace may not make sense. type CriticalError struct { // MainError is the primary error. Must be non-nil. MainError error - // DiagList contains any supplemental (structured) diagnostics. - DiagList []*Diagnostic + + // Diagnostics contains any supplemental (structured) diagnostics. + Diagnostics []*Diagnostic } // An Diagnostic corresponds to an LSP Diagnostic. @@ -652,6 +651,10 @@ type Diagnostic struct { Analyzer *Analyzer } +func (d *Diagnostic) String() string { + return fmt.Sprintf("%v: %s", d.Range, d.Message) +} + type DiagnosticSource string const ( @@ -662,6 +665,7 @@ const ( ModTidyError DiagnosticSource = "go mod tidy" OptimizationDetailsError DiagnosticSource = "optimizer details" UpgradeNotification DiagnosticSource = "upgrade available" + Vulncheck DiagnosticSource = "govulncheck" TemplateError DiagnosticSource = "template" WorkFileError DiagnosticSource = "go.work file" ) @@ -670,10 +674,6 @@ func AnalyzerErrorKind(name string) DiagnosticSource { return DiagnosticSource(name) } -var ( - PackagesLoadError = errors.New("packages.Load error") -) - // WorkspaceModuleVersion is the nonexistent pseudoversion suffix used in the // construction of the workspace module. It is exported so that we can make // sure not to show this version to end users in error messages, to avoid diff --git a/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go similarity index 69% rename from internal/lsp/source/workspace_symbol.go rename to gopls/internal/lsp/source/workspace_symbol.go index 11e22d17bea..ee4e020e257 100644 --- a/internal/lsp/source/workspace_symbol.go +++ b/gopls/internal/lsp/source/workspace_symbol.go @@ -8,16 +8,18 @@ import ( "context" "fmt" "go/types" + "path" "path/filepath" + "regexp" "runtime" "sort" "strings" "unicode" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/fuzzy" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/fuzzy" ) // Symbol holds a precomputed symbol value. Note: we avoid using the @@ -50,14 +52,26 @@ const maxSymbols = 100 // with a different configured SymbolMatcher per View. Therefore we assume that // Session level configuration will define the SymbolMatcher to be used for the // WorkspaceSymbols method. -func WorkspaceSymbols(ctx context.Context, matcherType SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) { +func WorkspaceSymbols(ctx context.Context, matcher SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) { ctx, done := event.Start(ctx, "source.WorkspaceSymbols") defer done() if query == "" { return nil, nil } - sc := newSymbolCollector(matcherType, style, query) - return sc.walk(ctx, views) + + var s symbolizer + switch style { + case DynamicSymbols: + s = dynamicSymbolMatch + case FullyQualifiedSymbols: + s = fullyQualifiedSymbolMatch + case PackageQualifiedSymbols: + s = packageSymbolMatch + default: + panic(fmt.Errorf("unknown symbol style: %v", style)) + } + + return collectSymbols(ctx, views, matcher, s, query) } // A matcherFunc returns the index and score of a symbol match. @@ -71,17 +85,25 @@ type matcherFunc func(chunks []string) (int, float64) // []string{"myType.field"} or []string{"myType.", "field"}. // // See the comment for symbolCollector for more information. -type symbolizer func(name string, pkg Metadata, m matcherFunc) ([]string, float64) +// +// The space argument is an empty slice with spare capacity that may be used +// to allocate the result. +type symbolizer func(space []string, name string, pkg Metadata, m matcherFunc) ([]string, float64) -func fullyQualifiedSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { - _, score := dynamicSymbolMatch(name, pkg, matcher) - if score > 0 { - return []string{pkg.PackagePath(), ".", name}, score +func fullyQualifiedSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { + if _, score := dynamicSymbolMatch(space, name, pkg, matcher); score > 0 { + return append(space, pkg.PackagePath(), ".", name), score } return nil, 0 } -func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { +func dynamicSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { + if IsCommandLineArguments(pkg.PackageID()) { + // command-line-arguments packages have a non-sensical package path, so + // just use their package name. + return packageSymbolMatch(space, name, pkg, matcher) + } + var score float64 endsInPkgName := strings.HasSuffix(pkg.PackagePath(), pkg.PackageName()) @@ -89,14 +111,14 @@ func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin // If the package path does not end in the package name, we need to check the // package-qualified symbol as an extra pass first. if !endsInPkgName { - pkgQualified := []string{pkg.PackageName(), ".", name} + pkgQualified := append(space, pkg.PackageName(), ".", name) idx, score := matcher(pkgQualified) nameStart := len(pkg.PackageName()) + 1 if score > 0 { // If our match is contained entirely within the unqualified portion, // just return that. if idx >= nameStart { - return []string{name}, score + return append(space, name), score } // Lower the score for matches that include the package name. return pkgQualified, score * 0.8 @@ -104,13 +126,13 @@ func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin } // Now try matching the fully qualified symbol. - fullyQualified := []string{pkg.PackagePath(), ".", name} + fullyQualified := append(space, pkg.PackagePath(), ".", name) idx, score := matcher(fullyQualified) // As above, check if we matched just the unqualified symbol name. nameStart := len(pkg.PackagePath()) + 1 if idx >= nameStart { - return []string{name}, score + return append(space, name), score } // If our package path ends in the package name, we'll have skipped the @@ -119,7 +141,7 @@ func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin if endsInPkgName && idx >= 0 { pkgStart := len(pkg.PackagePath()) - len(pkg.PackageName()) if idx >= pkgStart { - return []string{pkg.PackageName(), ".", name}, score + return append(space, pkg.PackageName(), ".", name), score } } @@ -128,51 +150,14 @@ func dynamicSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]strin return fullyQualified, score * 0.6 } -func packageSymbolMatch(name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { - qualified := []string{pkg.PackageName(), ".", name} +func packageSymbolMatch(space []string, name string, pkg Metadata, matcher matcherFunc) ([]string, float64) { + qualified := append(space, pkg.PackageName(), ".", name) if _, s := matcher(qualified); s > 0 { return qualified, s } return nil, 0 } -// symbolCollector holds context as we walk Packages, gathering symbols that -// match a given query. -// -// How we match symbols is parameterized by two interfaces: -// - A matcherFunc determines how well a string symbol matches a query. It -// returns a non-negative score indicating the quality of the match. A score -// of zero indicates no match. -// - A symbolizer determines how we extract the symbol for an object. This -// enables the 'symbolStyle' configuration option. -type symbolCollector struct { - // These types parameterize the symbol-matching pass. - matchers []matcherFunc - symbolizer symbolizer - - symbolStore -} - -func newSymbolCollector(matcher SymbolMatcher, style SymbolStyle, query string) *symbolCollector { - var s symbolizer - switch style { - case DynamicSymbols: - s = dynamicSymbolMatch - case FullyQualifiedSymbols: - s = fullyQualifiedSymbolMatch - case PackageQualifiedSymbols: - s = packageSymbolMatch - default: - panic(fmt.Errorf("unknown symbol style: %v", style)) - } - sc := &symbolCollector{symbolizer: s} - sc.matchers = make([]matcherFunc, runtime.GOMAXPROCS(-1)) - for i := range sc.matchers { - sc.matchers[i] = buildMatcher(matcher, query) - } - return sc -} - func buildMatcher(matcher SymbolMatcher, query string) matcherFunc { switch matcher { case SymbolFuzzy: @@ -302,36 +287,42 @@ func (c comboMatcher) match(chunks []string) (int, float64) { return first, score } -func (sc *symbolCollector) walk(ctx context.Context, views []View) ([]protocol.SymbolInformation, error) { - // Use the root view URIs for determining (lexically) whether a uri is in any - // open workspace. - var roots []string - for _, v := range views { - roots = append(roots, strings.TrimRight(string(v.Folder()), "/")) - } - - results := make(chan *symbolStore) - matcherlen := len(sc.matchers) - files := make(map[span.URI]symbolFile) +// collectSymbols calls snapshot.Symbols to walk the syntax trees of +// all files in the views' current snapshots, and returns a sorted, +// scored list of symbols that best match the parameters. +// +// How it matches symbols is parameterized by two interfaces: +// - A matcherFunc determines how well a string symbol matches a query. It +// returns a non-negative score indicating the quality of the match. A score +// of zero indicates no match. +// - A symbolizer determines how we extract the symbol for an object. This +// enables the 'symbolStyle' configuration option. +func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher, symbolizer symbolizer, query string) ([]protocol.SymbolInformation, error) { + // Extract symbols from all files. + var work []symbolFile + var roots []string + seen := make(map[span.URI]bool) + // TODO(adonovan): opt: parallelize this loop? How often is len > 1? for _, v := range views { snapshot, release := v.Snapshot(ctx) defer release() - psyms, err := snapshot.Symbols(ctx) - if err != nil { - return nil, err - } + + // Use the root view URIs for determining (lexically) + // whether a URI is in any open workspace. + roots = append(roots, strings.TrimRight(string(v.Folder()), "/")) filters := v.Options().DirectoryFilters + filterer := NewFilterer(filters) folder := filepath.ToSlash(v.Folder().Filename()) - for uri, syms := range psyms { + for uri, syms := range snapshot.Symbols(ctx) { norm := filepath.ToSlash(uri.Filename()) nm := strings.TrimPrefix(norm, folder) - if FiltersDisallow(nm, filters) { + if filterer.Disallow(nm) { continue } // Only scan each file once. - if _, ok := files[uri]; ok { + if seen[uri] { continue } mds, err := snapshot.MetadataForFile(ctx, uri) @@ -343,63 +334,110 @@ func (sc *symbolCollector) walk(ctx context.Context, views []View) ([]protocol.S // TODO: should use the bug reporting API continue } - files[uri] = symbolFile{uri, mds[0], syms} + seen[uri] = true + work = append(work, symbolFile{uri, mds[0], syms}) } } - var work []symbolFile - for _, f := range files { - work = append(work, f) - } - - // Compute matches concurrently. Each symbolWorker has its own symbolStore, + // Match symbols in parallel. + // Each worker has its own symbolStore, // which we merge at the end. - for i, matcher := range sc.matchers { - go func(i int, matcher matcherFunc) { - w := &symbolWorker{ - symbolizer: sc.symbolizer, - matcher: matcher, - ss: &symbolStore{}, - roots: roots, - } - for j := i; j < len(work); j += matcherlen { - w.matchFile(work[j]) + nmatchers := runtime.GOMAXPROCS(-1) // matching is CPU bound + results := make(chan *symbolStore) + for i := 0; i < nmatchers; i++ { + go func(i int) { + matcher := buildMatcher(matcherType, query) + 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]) } - results <- w.ss - }(i, matcher) + results <- store + }(i) } - for i := 0; i < matcherlen; i++ { - ss := <-results - for _, si := range ss.res { - sc.store(si) + // Gather and merge results as they arrive. + var unified symbolStore + for i := 0; i < nmatchers; i++ { + store := <-results + for _, syms := range store.res { + unified.store(syms) } } - return sc.results(), nil + return unified.results(), nil } -// FilterDisallow is code from the body of cache.pathExcludedByFilter in cache/view.go -// Exporting and using that function would cause an import cycle. -// Moving it here and exporting it would leave behind view_test.go. -// (This code is exported and used in the body of cache.pathExcludedByFilter) -func FiltersDisallow(path string, filters []string) bool { - path = strings.TrimPrefix(path, "/") - var excluded bool - for _, filter := range filters { +type Filterer struct { + // Whether a filter is excluded depends on the operator (first char of the raw filter). + // Slices filters and excluded then should have the same length. + filters []*regexp.Regexp + excluded []bool +} + +// NewFilterer computes regular expression form of all raw filters +func NewFilterer(rawFilters []string) *Filterer { + var f Filterer + for _, filter := range rawFilters { + filter = path.Clean(filepath.ToSlash(filter)) + // TODO(dungtuanle): fix: validate [+-] prefix. op, prefix := filter[0], filter[1:] - // Non-empty prefixes have to be precise directory matches. - if prefix != "" { - prefix = prefix + "/" - path = path + "/" - } - if !strings.HasPrefix(path, prefix) { - continue + // convertFilterToRegexp adds "/" at the end of prefix to handle cases where a filter is a prefix of another filter. + // For example, it prevents [+foobar, -foo] from excluding "foobar". + f.filters = append(f.filters, convertFilterToRegexp(filepath.ToSlash(prefix))) + f.excluded = append(f.excluded, op == '-') + } + + return &f +} + +// Disallow return true if the path is excluded from the filterer's filters. +func (f *Filterer) Disallow(path string) bool { + // Ensure trailing but not leading slash. + path = strings.TrimPrefix(path, "/") + if !strings.HasSuffix(path, "/") { + path += "/" + } + + // TODO(adonovan): opt: iterate in reverse and break at first match. + excluded := false + for i, filter := range f.filters { + if filter.MatchString(path) { + excluded = f.excluded[i] // last match wins } - excluded = op == '-' } return excluded } +// convertFilterToRegexp replaces glob-like operator substrings in a string file path to their equivalent regex forms. +// Supporting glob-like operators: +// - **: match zero or more complete path segments +func convertFilterToRegexp(filter string) *regexp.Regexp { + if filter == "" { + return regexp.MustCompile(".*") + } + var ret strings.Builder + ret.WriteString("^") + segs := strings.Split(filter, "/") + for _, seg := range segs { + // Inv: seg != "" since path is clean. + if seg == "**" { + ret.WriteString(".*") + } else { + ret.WriteString(regexp.QuoteMeta(seg)) + } + ret.WriteString("/") + } + pattern := ret.String() + + // Remove unnecessary "^.*" prefix, which increased + // BenchmarkWorkspaceSymbols time by ~20% (even though + // filter CPU time increased by only by ~2.5%) when the + // default filter was changed to "**/node_modules". + pattern = strings.TrimPrefix(pattern, "^.*") + + return regexp.MustCompile(pattern) +} + // symbolFile holds symbol information for a single file. type symbolFile struct { uri span.URI @@ -407,20 +445,14 @@ type symbolFile struct { syms []Symbol } -// symbolWorker matches symbols and captures the highest scoring results. -type symbolWorker struct { - symbolizer symbolizer - matcher matcherFunc - ss *symbolStore - roots []string -} - -func (w *symbolWorker) matchFile(i symbolFile) { +// matchFile scans a symbol file and adds matching symbols to the store. +func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, roots []string, i symbolFile) { + space := make([]string, 0, 3) for _, sym := range i.syms { - symbolParts, score := w.symbolizer(sym.Name, i.md, w.matcher) + symbolParts, score := symbolizer(space, sym.Name, i.md, matcher) // Check if the score is too low before applying any downranking. - if w.ss.tooLow(score) { + if store.tooLow(score) { continue } @@ -463,7 +495,7 @@ func (w *symbolWorker) matchFile(i symbolFile) { } inWorkspace := false - for _, root := range w.roots { + for _, root := range roots { if strings.HasPrefix(string(i.uri), root) { inWorkspace = true break @@ -484,7 +516,7 @@ func (w *symbolWorker) matchFile(i symbolFile) { } score *= 1.0 - depth*depthFactor - if w.ss.tooLow(score) { + if store.tooLow(score) { continue } @@ -496,7 +528,7 @@ func (w *symbolWorker) matchFile(i symbolFile) { rng: sym.Range, container: i.md.PackagePath(), } - w.ss.store(si) + store.store(si) } } diff --git a/gopls/internal/lsp/source/workspace_symbol_test.go b/gopls/internal/lsp/source/workspace_symbol_test.go new file mode 100644 index 00000000000..24fb8b45210 --- /dev/null +++ b/gopls/internal/lsp/source/workspace_symbol_test.go @@ -0,0 +1,136 @@ +// 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 source + +import ( + "testing" +) + +func TestParseQuery(t *testing.T) { + tests := []struct { + query, s string + wantMatch bool + }{ + {"", "anything", false}, + {"any", "anything", true}, + {"any$", "anything", false}, + {"ing$", "anything", true}, + {"ing$", "anythinG", true}, + {"inG$", "anything", false}, + {"^any", "anything", true}, + {"^any", "Anything", true}, + {"^Any", "anything", false}, + {"at", "anything", true}, + // TODO: this appears to be a bug in the fuzzy matching algorithm. 'At' + // should cause a case-sensitive match. + // {"At", "anything", false}, + {"At", "Anything", true}, + {"'yth", "Anything", true}, + {"'yti", "Anything", false}, + {"'any 'thing", "Anything", true}, + {"anythn nythg", "Anything", true}, + {"ntx", "Anything", false}, + {"anythn", "anything", true}, + {"ing", "anything", true}, + {"anythn nythgx", "anything", false}, + } + + for _, test := range tests { + matcher := parseQuery(test.query, newFuzzyMatcher) + if _, score := matcher([]string{test.s}); score > 0 != test.wantMatch { + t.Errorf("parseQuery(%q) match for %q: %.2g, want match: %t", test.query, test.s, score, test.wantMatch) + } + } +} + +func TestFiltererDisallow(t *testing.T) { + tests := []struct { + filters []string + included []string + excluded []string + }{ + { + []string{"+**/c.go"}, + []string{"a/c.go", "a/b/c.go"}, + []string{}, + }, + { + []string{"+a/**/c.go"}, + []string{"a/b/c.go", "a/b/d/c.go", "a/c.go"}, + []string{}, + }, + { + []string{"-a/c.go", "+a/**"}, + []string{"a/c.go"}, + []string{}, + }, + { + []string{"+a/**/c.go", "-**/c.go"}, + []string{}, + []string{"a/b/c.go"}, + }, + { + []string{"+a/**/c.go", "-a/**"}, + []string{}, + []string{"a/b/c.go"}, + }, + { + []string{"+**/c.go", "-a/**/c.go"}, + []string{}, + []string{"a/b/c.go"}, + }, + { + []string{"+foobar", "-foo"}, + []string{"foobar", "foobar/a"}, + []string{"foo", "foo/a"}, + }, + { + []string{"+", "-"}, + []string{}, + []string{"foobar", "foobar/a", "foo", "foo/a"}, + }, + { + []string{"-", "+"}, + []string{"foobar", "foobar/a", "foo", "foo/a"}, + []string{}, + }, + { + []string{"-a/**/b/**/c.go"}, + []string{}, + []string{"a/x/y/z/b/f/g/h/c.go"}, + }, + // tests for unsupported glob operators + { + []string{"+**/c.go", "-a/*/c.go"}, + []string{"a/b/c.go"}, + []string{}, + }, + { + []string{"+**/c.go", "-a/?/c.go"}, + []string{"a/b/c.go"}, + []string{}, + }, + { + []string{"-b"}, // should only filter paths prefixed with the "b" directory + []string{"a/b/c.go", "bb"}, + []string{"b/c/d.go", "b"}, + }, + } + + for _, test := range tests { + filterer := NewFilterer(test.filters) + for _, inc := range test.included { + if filterer.Disallow(inc) { + t.Errorf("Filters %v excluded %v, wanted included", test.filters, inc) + } + } + + for _, exc := range test.excluded { + if !filterer.Disallow(exc) { + t.Errorf("Filters %v included %v, wanted excluded", test.filters, exc) + } + } + } +} diff --git a/internal/lsp/symbols.go b/gopls/internal/lsp/symbols.go similarity index 89% rename from internal/lsp/symbols.go rename to gopls/internal/lsp/symbols.go index f04e4572dba..0cc0b33a5b5 100644 --- a/internal/lsp/symbols.go +++ b/gopls/internal/lsp/symbols.go @@ -8,10 +8,10 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/template" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/template" ) func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) { diff --git a/internal/lsp/template/completion.go b/gopls/internal/lsp/template/completion.go similarity index 94% rename from internal/lsp/template/completion.go rename to gopls/internal/lsp/template/completion.go index 13dbdf1e525..140c674747d 100644 --- a/internal/lsp/template/completion.go +++ b/gopls/internal/lsp/template/completion.go @@ -12,8 +12,8 @@ import ( "go/token" "strings" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) // information needed for completion @@ -285,17 +285,3 @@ func weakMatch(choice, pattern string) float64 { } return 1 } - -// for debug printing -func strContext(c protocol.CompletionContext) string { - switch c.TriggerKind { - case protocol.Invoked: - return "invoked" - case protocol.TriggerCharacter: - return fmt.Sprintf("triggered(%s)", c.TriggerCharacter) - case protocol.TriggerForIncompleteCompletions: - // gopls doesn't seem to handle these explicitly anywhere - return "incomplete" - } - return fmt.Sprintf("?%v", c) -} diff --git a/internal/lsp/template/completion_test.go b/gopls/internal/lsp/template/completion_test.go similarity index 98% rename from internal/lsp/template/completion_test.go rename to gopls/internal/lsp/template/completion_test.go index bfcdb537202..0fc478842ee 100644 --- a/internal/lsp/template/completion_test.go +++ b/gopls/internal/lsp/template/completion_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) func init() { diff --git a/internal/lsp/template/highlight.go b/gopls/internal/lsp/template/highlight.go similarity index 96% rename from internal/lsp/template/highlight.go rename to gopls/internal/lsp/template/highlight.go index a45abaf5020..1e06b92085e 100644 --- a/internal/lsp/template/highlight.go +++ b/gopls/internal/lsp/template/highlight.go @@ -9,8 +9,8 @@ import ( "fmt" "regexp" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Highlight(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.DocumentHighlight, error) { diff --git a/internal/lsp/template/implementations.go b/gopls/internal/lsp/template/implementations.go similarity index 97% rename from internal/lsp/template/implementations.go rename to gopls/internal/lsp/template/implementations.go index cda3e7ef0a5..6c90b68cd5c 100644 --- a/internal/lsp/template/implementations.go +++ b/gopls/internal/lsp/template/implementations.go @@ -11,9 +11,9 @@ import ( "strconv" "time" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) // line number (1-based) and message diff --git a/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go similarity index 97% rename from internal/lsp/template/parse.go rename to gopls/internal/lsp/template/parse.go index 181a5228fd2..06b7568094d 100644 --- a/internal/lsp/template/parse.go +++ b/gopls/internal/lsp/template/parse.go @@ -24,10 +24,10 @@ import ( "text/template/parse" "unicode/utf8" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" ) var ( @@ -492,15 +492,6 @@ func (wr wrNode) writeNode(n parse.Node, indent string) { } } -// short prints at most 40 bytes of node.String(), for debugging -func short(n parse.Node) string { - s := fmt.Sprint(n) // recovers from panic - if len(s) > 40 { - return s[:40] + "..." - } - return s -} - var kindNames = []string{"", "File", "Module", "Namespace", "Package", "Class", "Method", "Property", "Field", "Constructor", "Enum", "Interface", "Function", "Variable", "Constant", "String", "Number", "Boolean", "Array", "Object", "Key", "Null", "EnumMember", "Struct", "Event", diff --git a/internal/lsp/template/parse_test.go b/gopls/internal/lsp/template/parse_test.go similarity index 100% rename from internal/lsp/template/parse_test.go rename to gopls/internal/lsp/template/parse_test.go diff --git a/internal/lsp/template/symbols.go b/gopls/internal/lsp/template/symbols.go similarity index 98% rename from internal/lsp/template/symbols.go rename to gopls/internal/lsp/template/symbols.go index ce5a1e799b7..24f9604c100 100644 --- a/internal/lsp/template/symbols.go +++ b/gopls/internal/lsp/template/symbols.go @@ -12,8 +12,8 @@ import ( "unicode/utf8" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) // in local coordinates, to be translated to protocol.DocumentSymbol diff --git a/internal/lsp/testdata/%percent/perc%ent.go b/gopls/internal/lsp/testdata/%percent/perc%ent.go similarity index 100% rename from internal/lsp/testdata/%percent/perc%ent.go rename to gopls/internal/lsp/testdata/%percent/perc%ent.go diff --git a/internal/lsp/testdata/addimport/addimport.go.golden b/gopls/internal/lsp/testdata/addimport/addimport.go.golden similarity index 100% rename from internal/lsp/testdata/addimport/addimport.go.golden rename to gopls/internal/lsp/testdata/addimport/addimport.go.golden diff --git a/internal/lsp/testdata/addimport/addimport.go.in b/gopls/internal/lsp/testdata/addimport/addimport.go.in similarity index 100% rename from internal/lsp/testdata/addimport/addimport.go.in rename to gopls/internal/lsp/testdata/addimport/addimport.go.in diff --git a/internal/lsp/testdata/address/address.go b/gopls/internal/lsp/testdata/address/address.go similarity index 100% rename from internal/lsp/testdata/address/address.go rename to gopls/internal/lsp/testdata/address/address.go diff --git a/internal/lsp/testdata/analyzer/bad_test.go b/gopls/internal/lsp/testdata/analyzer/bad_test.go similarity index 63% rename from internal/lsp/testdata/analyzer/bad_test.go rename to gopls/internal/lsp/testdata/analyzer/bad_test.go index c819cbc0111..b1724c66693 100644 --- a/internal/lsp/testdata/analyzer/bad_test.go +++ b/gopls/internal/lsp/testdata/analyzer/bad_test.go @@ -4,15 +4,21 @@ import ( "fmt" "sync" "testing" + "time" ) func Testbad(t *testing.T) { //@diag("", "tests", "Testbad has malformed name: first letter after 'Test' must not be lowercase", "warning") var x sync.Mutex _ = x //@diag("x", "copylocks", "assignment copies lock value to _: sync.Mutex", "warning") - printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, "printf", "golang.org/x/tools/internal/lsp/analyzer.printfWrapper format %s reads arg #1, but call has 0 args", "warning") + printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, "printf", "golang.org/lsptests/analyzer.printfWrapper format %s reads arg #1, but call has 0 args", "warning") } func printfWrapper(format string, args ...interface{}) { fmt.Printf(format, args...) } + +func _() { + now := time.Now() + fmt.Println(now.Format("2006-02-01")) //@diag("2006-02-01", "timeformat", "2006-02-01 should be 2006-01-02", "warning") +} diff --git a/internal/lsp/testdata/anon/anon.go.in b/gopls/internal/lsp/testdata/anon/anon.go.in similarity index 100% rename from internal/lsp/testdata/anon/anon.go.in rename to gopls/internal/lsp/testdata/anon/anon.go.in diff --git a/internal/lsp/testdata/append/append.go b/gopls/internal/lsp/testdata/append/append.go similarity index 100% rename from internal/lsp/testdata/append/append.go rename to gopls/internal/lsp/testdata/append/append.go diff --git a/internal/lsp/testdata/append/append2.go.in b/gopls/internal/lsp/testdata/append/append2.go.in similarity index 100% rename from internal/lsp/testdata/append/append2.go.in rename to gopls/internal/lsp/testdata/append/append2.go.in diff --git a/internal/lsp/testdata/arraytype/array_type.go.in b/gopls/internal/lsp/testdata/arraytype/array_type.go.in similarity index 83% rename from internal/lsp/testdata/arraytype/array_type.go.in rename to gopls/internal/lsp/testdata/arraytype/array_type.go.in index 7e9a96f7b0d..ac1a3e78297 100644 --- a/internal/lsp/testdata/arraytype/array_type.go.in +++ b/gopls/internal/lsp/testdata/arraytype/array_type.go.in @@ -1,7 +1,7 @@ package arraytype import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { @@ -9,7 +9,8 @@ func _() { val string //@item(atVal, "val", "string", "var") ) - [] //@complete(" //", PackageFoo) + // disabled - see issue #54822 + [] // complete(" //", PackageFoo) []val //@complete(" //") @@ -33,7 +34,8 @@ func _() { var s []myInt //@item(atS, "s", "[]myInt", "var") s = []m //@complete(" //", atMyInt) - s = [] //@complete(" //", atMyInt, PackageFoo) + // disabled - see issue #54822 + s = [] // complete(" //", atMyInt, PackageFoo) var a [1]myInt a = [1]m //@complete(" //", atMyInt) diff --git a/internal/lsp/testdata/assign/assign.go.in b/gopls/internal/lsp/testdata/assign/assign.go.in similarity index 89% rename from internal/lsp/testdata/assign/assign.go.in rename to gopls/internal/lsp/testdata/assign/assign.go.in index 8c00ae9e0e5..93a622c8326 100644 --- a/internal/lsp/testdata/assign/assign.go.in +++ b/gopls/internal/lsp/testdata/assign/assign.go.in @@ -1,6 +1,6 @@ package assign -import "golang.org/x/tools/internal/lsp/assign/internal/secret" +import "golang.org/lsptests/assign/internal/secret" func _() { secret.Hello() diff --git a/internal/lsp/testdata/assign/internal/secret/secret.go b/gopls/internal/lsp/testdata/assign/internal/secret/secret.go similarity index 100% rename from internal/lsp/testdata/assign/internal/secret/secret.go rename to gopls/internal/lsp/testdata/assign/internal/secret/secret.go diff --git a/gopls/internal/lsp/testdata/bad/bad0.go b/gopls/internal/lsp/testdata/bad/bad0.go new file mode 100644 index 00000000000..9eedf4aead0 --- /dev/null +++ b/gopls/internal/lsp/testdata/bad/bad0.go @@ -0,0 +1,24 @@ +//go:build go1.11 +// +build go1.11 + +package bad + +import _ "golang.org/lsptests/assign/internal/secret" //@diag("\"golang.org/lsptests/assign/internal/secret\"", "compiler", "could not import golang.org/lsptests/assign/internal/secret \\(invalid use of internal package golang.org/lsptests/assign/internal/secret\\)", "error") + +func stuff() { //@item(stuff, "stuff", "func()", "func") + x := "heeeeyyyy" + random2(x) //@diag("x", "compiler", "cannot use x \\(variable of type string\\) as int value in argument to random2", "error") + random2(1) //@complete("dom", random, random2, random3) + y := 3 //@diag("y", "compiler", "y declared (and|but) not used", "error") +} + +type bob struct { //@item(bob, "bob", "struct{...}", "struct") + x int +} + +func _() { + var q int + _ = &bob{ + f: q, //@diag("f: q", "compiler", "unknown field f in struct literal", "error") + } +} diff --git a/internal/lsp/testdata/bad/bad1.go b/gopls/internal/lsp/testdata/bad/bad1.go similarity index 56% rename from internal/lsp/testdata/bad/bad1.go rename to gopls/internal/lsp/testdata/bad/bad1.go index 512f2d9869b..13b3d0af61c 100644 --- a/internal/lsp/testdata/bad/bad1.go +++ b/gopls/internal/lsp/testdata/bad/bad1.go @@ -1,3 +1,4 @@ +//go:build go1.11 // +build go1.11 package bad @@ -5,7 +6,7 @@ package bad // See #36637 type stateFunc func() stateFunc //@item(stateFunc, "stateFunc", "func() stateFunc", "type") -var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", "compiler", "undeclared name: unknown", "error") +var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", "compiler", "(undeclared name|undefined): unknown", "error") func random() int { //@item(random, "random", "func() int", "func") //@complete("", global_a, bob, random, random2, random3, stateFunc, stuff) @@ -13,9 +14,9 @@ func random() int { //@item(random, "random", "func() int", "func") } func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func"),item(bad_y_param, "y", "int", "var") - x := 6 //@item(x, "x", "int", "var"),diag("x", "compiler", "x declared but not used", "error") - var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared but not used", "error"),diag("blah", "compiler", "undeclared name: blah", "error") - var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared but not used", "error"),diag("blob", "compiler", "undeclared name: blob", "error") + x := 6 //@item(x, "x", "int", "var"),diag("x", "compiler", "x declared (and|but) not used", "error") + var q blah //@item(q, "q", "blah", "var"),diag("q", "compiler", "q declared (and|but) not used", "error"),diag("blah", "compiler", "(undeclared name|undefined): blah", "error") + var t **blob //@item(t, "t", "**blob", "var"),diag("t", "compiler", "t declared (and|but) not used", "error"),diag("blob", "compiler", "(undeclared name|undefined): blob", "error") //@complete("", q, t, x, bad_y_param, global_a, bob, random, random2, random3, stateFunc, stuff) return y @@ -24,10 +25,10 @@ func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func") func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),item(y_variadic_param, "y", "[]int", "var") //@complete("", y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) - var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared but not used", "error"),diag("favType1", "compiler", "undeclared name: favType1", "error") - var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared but not used", "error"),diag("keyType", "compiler", "undeclared name: keyType", "error") - var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared but not used", "error"),diag("favType2", "compiler", "undeclared name: favType2", "error") - var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared but not used", "error"),diag("badResult", "compiler", "undeclared name: badResult", "error") - var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared but not used", "error"),diag("badParam", "compiler", "undeclared name: badParam", "error") + var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", "compiler", "ch declared (and|but) not used", "error"),diag("favType1", "compiler", "(undeclared name|undefined): favType1", "error") + var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", "compiler", "m declared (and|but) not used", "error"),diag("keyType", "compiler", "(undeclared name|undefined): keyType", "error") + var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", "compiler", "arr declared (and|but) not used", "error"),diag("favType2", "compiler", "(undeclared name|undefined): favType2", "error") + var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", "compiler", "fn1 declared (and|but) not used", "error"),diag("badResult", "compiler", "(undeclared name|undefined): badResult", "error") + var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", "compiler", "fn2 declared (and|but) not used", "error"),diag("badParam", "compiler", "(undeclared name|undefined): badParam", "error") //@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) } diff --git a/internal/lsp/testdata/badstmt/badstmt.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt.go.in similarity index 75% rename from internal/lsp/testdata/badstmt/badstmt.go.in rename to gopls/internal/lsp/testdata/badstmt/badstmt.go.in index 5a560791086..8f0654a8a52 100644 --- a/internal/lsp/testdata/badstmt/badstmt.go.in +++ b/gopls/internal/lsp/testdata/badstmt/badstmt.go.in @@ -1,11 +1,11 @@ package badstmt import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { - defer foo.F //@complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement", "error") + defer foo.F //@complete(" //", Foo),diag(" //", "syntax", "function must be invoked in defer statement|expression in defer must be function call", "error") y := 1 defer foo.F //@complete(" //", Foo) } diff --git a/internal/lsp/testdata/badstmt/badstmt_2.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_2.go.in similarity index 68% rename from internal/lsp/testdata/badstmt/badstmt_2.go.in rename to gopls/internal/lsp/testdata/badstmt/badstmt_2.go.in index f754b46aaac..6af9c35e3cf 100644 --- a/internal/lsp/testdata/badstmt/badstmt_2.go.in +++ b/gopls/internal/lsp/testdata/badstmt/badstmt_2.go.in @@ -1,7 +1,7 @@ package badstmt import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/badstmt/badstmt_3.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_3.go.in similarity index 75% rename from internal/lsp/testdata/badstmt/badstmt_3.go.in rename to gopls/internal/lsp/testdata/badstmt/badstmt_3.go.in index be774e84b05..d135e201505 100644 --- a/internal/lsp/testdata/badstmt/badstmt_3.go.in +++ b/gopls/internal/lsp/testdata/badstmt/badstmt_3.go.in @@ -1,7 +1,7 @@ package badstmt import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/badstmt/badstmt_4.go.in b/gopls/internal/lsp/testdata/badstmt/badstmt_4.go.in similarity index 71% rename from internal/lsp/testdata/badstmt/badstmt_4.go.in rename to gopls/internal/lsp/testdata/badstmt/badstmt_4.go.in index a9b46fb021b..6afd635ec2d 100644 --- a/internal/lsp/testdata/badstmt/badstmt_4.go.in +++ b/gopls/internal/lsp/testdata/badstmt/badstmt_4.go.in @@ -1,7 +1,7 @@ package badstmt import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/bar/bar.go.in b/gopls/internal/lsp/testdata/bar/bar.go.in similarity index 90% rename from internal/lsp/testdata/bar/bar.go.in rename to gopls/internal/lsp/testdata/bar/bar.go.in index c0f4b4c45c2..502bdf74060 100644 --- a/internal/lsp/testdata/bar/bar.go.in +++ b/gopls/internal/lsp/testdata/bar/bar.go.in @@ -3,7 +3,7 @@ package bar import ( - "golang.org/x/tools/internal/lsp/foo" //@item(foo, "foo", "\"golang.org/x/tools/internal/lsp/foo\"", "package") + "golang.org/lsptests/foo" //@item(foo, "foo", "\"golang.org/lsptests/foo\"", "package") ) func helper(i foo.IntFoo) {} //@item(helper, "helper", "func(i foo.IntFoo)", "func") diff --git a/internal/lsp/testdata/basiclit/basiclit.go b/gopls/internal/lsp/testdata/basiclit/basiclit.go similarity index 100% rename from internal/lsp/testdata/basiclit/basiclit.go rename to gopls/internal/lsp/testdata/basiclit/basiclit.go diff --git a/internal/lsp/testdata/baz/baz.go.in b/gopls/internal/lsp/testdata/baz/baz.go.in similarity index 87% rename from internal/lsp/testdata/baz/baz.go.in rename to gopls/internal/lsp/testdata/baz/baz.go.in index 3b74ee580c3..94952e1267b 100644 --- a/internal/lsp/testdata/baz/baz.go.in +++ b/gopls/internal/lsp/testdata/baz/baz.go.in @@ -3,9 +3,9 @@ package baz import ( - "golang.org/x/tools/internal/lsp/bar" + "golang.org/lsptests/bar" - f "golang.org/x/tools/internal/lsp/foo" + f "golang.org/lsptests/foo" ) var FooStruct f.StructFoo diff --git a/internal/lsp/testdata/builtins/builtin_args.go b/gopls/internal/lsp/testdata/builtins/builtin_args.go similarity index 100% rename from internal/lsp/testdata/builtins/builtin_args.go rename to gopls/internal/lsp/testdata/builtins/builtin_args.go diff --git a/internal/lsp/testdata/builtins/builtin_types.go b/gopls/internal/lsp/testdata/builtins/builtin_types.go similarity index 100% rename from internal/lsp/testdata/builtins/builtin_types.go rename to gopls/internal/lsp/testdata/builtins/builtin_types.go diff --git a/internal/lsp/testdata/builtins/builtins.go b/gopls/internal/lsp/testdata/builtins/builtins.go similarity index 100% rename from internal/lsp/testdata/builtins/builtins.go rename to gopls/internal/lsp/testdata/builtins/builtins.go diff --git a/internal/lsp/testdata/builtins/constants.go b/gopls/internal/lsp/testdata/builtins/constants.go similarity index 100% rename from internal/lsp/testdata/builtins/constants.go rename to gopls/internal/lsp/testdata/builtins/constants.go diff --git a/internal/lsp/testdata/callhierarchy/callhierarchy.go b/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go similarity index 95% rename from internal/lsp/testdata/callhierarchy/callhierarchy.go rename to gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go index 58c23bdd634..252e8054f40 100644 --- a/internal/lsp/testdata/callhierarchy/callhierarchy.go +++ b/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go @@ -4,7 +4,7 @@ package callhierarchy -import "golang.org/x/tools/internal/lsp/callhierarchy/outgoing" +import "golang.org/lsptests/callhierarchy/outgoing" func a() { //@mark(hierarchyA, "a") D() diff --git a/internal/lsp/testdata/callhierarchy/incoming/incoming.go b/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go similarity index 84% rename from internal/lsp/testdata/callhierarchy/incoming/incoming.go rename to gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go index 3bfb4ad998d..c629aa87929 100644 --- a/internal/lsp/testdata/callhierarchy/incoming/incoming.go +++ b/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go @@ -4,7 +4,7 @@ package incoming -import "golang.org/x/tools/internal/lsp/callhierarchy" +import "golang.org/lsptests/callhierarchy" // A is exported to test incoming calls across packages func A() { //@mark(incomingA, "A") diff --git a/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go b/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go similarity index 100% rename from internal/lsp/testdata/callhierarchy/outgoing/outgoing.go rename to gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go diff --git a/internal/lsp/testdata/casesensitive/casesensitive.go b/gopls/internal/lsp/testdata/casesensitive/casesensitive.go similarity index 100% rename from internal/lsp/testdata/casesensitive/casesensitive.go rename to gopls/internal/lsp/testdata/casesensitive/casesensitive.go diff --git a/internal/lsp/testdata/cast/cast.go.in b/gopls/internal/lsp/testdata/cast/cast.go.in similarity index 100% rename from internal/lsp/testdata/cast/cast.go.in rename to gopls/internal/lsp/testdata/cast/cast.go.in diff --git a/internal/lsp/testdata/cgo/declarecgo.go b/gopls/internal/lsp/testdata/cgo/declarecgo.go similarity index 100% rename from internal/lsp/testdata/cgo/declarecgo.go rename to gopls/internal/lsp/testdata/cgo/declarecgo.go diff --git a/internal/lsp/testdata/cgo/declarecgo.go.golden b/gopls/internal/lsp/testdata/cgo/declarecgo.go.golden similarity index 58% rename from internal/lsp/testdata/cgo/declarecgo.go.golden rename to gopls/internal/lsp/testdata/cgo/declarecgo.go.golden index b6d94d0c6c6..0d6fbb0fff6 100644 --- a/internal/lsp/testdata/cgo/declarecgo.go.golden +++ b/gopls/internal/lsp/testdata/cgo/declarecgo.go.golden @@ -3,7 +3,7 @@ cgo/declarecgo.go:18:6-13: defined here as ```go func Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example) -- funccgoexample-definition-json -- { "span": { @@ -19,7 +19,7 @@ func Example() "offset": 158 } }, - "description": "```go\nfunc Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example)" + "description": "```go\nfunc Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)" } -- funccgoexample-hoverdef -- @@ -27,4 +27,4 @@ func Example() func Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example) diff --git a/internal/lsp/testdata/cgo/declarecgo_nocgo.go b/gopls/internal/lsp/testdata/cgo/declarecgo_nocgo.go similarity index 100% rename from internal/lsp/testdata/cgo/declarecgo_nocgo.go rename to gopls/internal/lsp/testdata/cgo/declarecgo_nocgo.go diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.golden b/gopls/internal/lsp/testdata/cgoimport/usecgo.go.golden similarity index 58% rename from internal/lsp/testdata/cgoimport/usecgo.go.golden rename to gopls/internal/lsp/testdata/cgoimport/usecgo.go.golden index f33f94f84a6..03fc22468ca 100644 --- a/internal/lsp/testdata/cgoimport/usecgo.go.golden +++ b/gopls/internal/lsp/testdata/cgoimport/usecgo.go.golden @@ -3,7 +3,7 @@ cgo/declarecgo.go:18:6-13: defined here as ```go func cgo.Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example) -- funccgoexample-definition-json -- { "span": { @@ -19,7 +19,7 @@ func cgo.Example() "offset": 158 } }, - "description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example)" + "description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example)" } -- funccgoexample-hoverdef -- @@ -27,4 +27,4 @@ func cgo.Example() func cgo.Example() ``` -[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example) +[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/cgo#Example) diff --git a/internal/lsp/testdata/cgoimport/usecgo.go.in b/gopls/internal/lsp/testdata/cgoimport/usecgo.go.in similarity index 76% rename from internal/lsp/testdata/cgoimport/usecgo.go.in rename to gopls/internal/lsp/testdata/cgoimport/usecgo.go.in index f258682ea13..414a739da99 100644 --- a/internal/lsp/testdata/cgoimport/usecgo.go.in +++ b/gopls/internal/lsp/testdata/cgoimport/usecgo.go.in @@ -1,7 +1,7 @@ package cgoimport import ( - "golang.org/x/tools/internal/lsp/cgo" + "golang.org/lsptests/cgo" ) func _() { diff --git a/internal/lsp/testdata/channel/channel.go b/gopls/internal/lsp/testdata/channel/channel.go similarity index 100% rename from internal/lsp/testdata/channel/channel.go rename to gopls/internal/lsp/testdata/channel/channel.go diff --git a/internal/lsp/testdata/codelens/codelens_test.go b/gopls/internal/lsp/testdata/codelens/codelens_test.go similarity index 100% rename from internal/lsp/testdata/codelens/codelens_test.go rename to gopls/internal/lsp/testdata/codelens/codelens_test.go diff --git a/internal/lsp/testdata/comment_completion/comment_completion.go.in b/gopls/internal/lsp/testdata/comment_completion/comment_completion.go.in similarity index 100% rename from internal/lsp/testdata/comment_completion/comment_completion.go.in rename to gopls/internal/lsp/testdata/comment_completion/comment_completion.go.in diff --git a/internal/lsp/testdata/complit/complit.go.in b/gopls/internal/lsp/testdata/complit/complit.go.in similarity index 100% rename from internal/lsp/testdata/complit/complit.go.in rename to gopls/internal/lsp/testdata/complit/complit.go.in diff --git a/internal/lsp/testdata/constant/constant.go b/gopls/internal/lsp/testdata/constant/constant.go similarity index 100% rename from internal/lsp/testdata/constant/constant.go rename to gopls/internal/lsp/testdata/constant/constant.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_for.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_for.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_for.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_for_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_for_init.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_for_init.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_if.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_if.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_if.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_if_eof.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_eof.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_if_eof.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_if_eof.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_if_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_if_init.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_if_init.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_multiline_if.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_selector_1.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_1.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_selector_1.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_selector_1.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_selector_2.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go similarity index 69% rename from internal/lsp/testdata/danglingstmt/dangling_selector_2.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go index a9e75e82a57..8d4b15bff6a 100644 --- a/internal/lsp/testdata/danglingstmt/dangling_selector_2.go +++ b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go @@ -1,6 +1,6 @@ package danglingstmt -import "golang.org/x/tools/internal/lsp/foo" +import "golang.org/lsptests/foo" func _() { foo. //@rank(" //", Foo) diff --git a/internal/lsp/testdata/danglingstmt/dangling_switch_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_switch_init.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init.go diff --git a/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go similarity index 100% rename from internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go rename to gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go diff --git a/internal/lsp/testdata/deep/deep.go b/gopls/internal/lsp/testdata/deep/deep.go similarity index 92% rename from internal/lsp/testdata/deep/deep.go rename to gopls/internal/lsp/testdata/deep/deep.go index 6ed5ff83999..6908824f82f 100644 --- a/internal/lsp/testdata/deep/deep.go +++ b/gopls/internal/lsp/testdata/deep/deep.go @@ -28,6 +28,13 @@ func _() { wantsContext(c) //@rank(")", ctxBackground),rank(")", ctxTODO) } +func _() { + var cork struct{ err error } + cork.err //@item(deepCorkErr, "cork.err", "error", "field") + context //@item(deepContextPkg, "context", "\"context\"", "package") + var _ error = co //@rank(" //", deepCorkErr, deepContextPkg) +} + func _() { // deepCircle is circular. type deepCircle struct { diff --git a/internal/lsp/testdata/errors/errors.go b/gopls/internal/lsp/testdata/errors/errors.go similarity index 73% rename from internal/lsp/testdata/errors/errors.go rename to gopls/internal/lsp/testdata/errors/errors.go index 42105629eaa..e14cde69e9e 100644 --- a/internal/lsp/testdata/errors/errors.go +++ b/gopls/internal/lsp/testdata/errors/errors.go @@ -1,7 +1,7 @@ package errors import ( - "golang.org/x/tools/internal/lsp/types" + "golang.org/lsptests/types" ) func _() { diff --git a/internal/lsp/testdata/extract/extract_function/extract_args_returns.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_args_returns.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_args_returns.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_args_returns.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_args_returns.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_args_returns.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_args_returns.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_args_returns.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_basic.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_basic.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_basic.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_basic.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_basic.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_basic.go.golden diff --git a/gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go new file mode 100644 index 00000000000..71f969e48c8 --- /dev/null +++ b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go @@ -0,0 +1,12 @@ +package extract + +func _() { + a := /* comment in the middle of a line */ 1 //@mark(exSt18, "a") + // Comment on its own line //@mark(exSt19, "Comment") + _ = 3 + 4 //@mark(exEn18, "4"),mark(exEn19, "4"),mark(exSt20, "_") + // Comment right after 3 + 4 + + // Comment after with space //@mark(exEn20, "Comment") + + //@extractfunc(exSt18, exEn18),extractfunc(exSt19, exEn19),extractfunc(exSt20, exEn20) +} diff --git a/gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden new file mode 100644 index 00000000000..1b2869ef7f5 --- /dev/null +++ b/gopls/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden @@ -0,0 +1,57 @@ +-- functionextraction_extract_basic_comment_4_2 -- +package extract + +func _() { + /* comment in the middle of a line */ + //@mark(exSt18, "a") + // Comment on its own line //@mark(exSt19, "Comment") + newFunction() //@mark(exEn18, "4"),mark(exEn19, "4"),mark(exSt20, "_") + // Comment right after 3 + 4 + + // Comment after with space //@mark(exEn20, "Comment") + + //@extractfunc(exSt18, exEn18),extractfunc(exSt19, exEn19),extractfunc(exSt20, exEn20) +} + +func newFunction() { + a := 1 + + _ = 3 + 4 +} + +-- functionextraction_extract_basic_comment_5_5 -- +package extract + +func _() { + a := /* comment in the middle of a line */ 1 //@mark(exSt18, "a") + // Comment on its own line //@mark(exSt19, "Comment") + newFunction() //@mark(exEn18, "4"),mark(exEn19, "4"),mark(exSt20, "_") + // Comment right after 3 + 4 + + // Comment after with space //@mark(exEn20, "Comment") + + //@extractfunc(exSt18, exEn18),extractfunc(exSt19, exEn19),extractfunc(exSt20, exEn20) +} + +func newFunction() { + _ = 3 + 4 +} + +-- functionextraction_extract_basic_comment_6_2 -- +package extract + +func _() { + a := /* comment in the middle of a line */ 1 //@mark(exSt18, "a") + // Comment on its own line //@mark(exSt19, "Comment") + newFunction() //@mark(exEn18, "4"),mark(exEn19, "4"),mark(exSt20, "_") + // Comment right after 3 + 4 + + // Comment after with space //@mark(exEn20, "Comment") + + //@extractfunc(exSt18, exEn18),extractfunc(exSt19, exEn19),extractfunc(exSt20, exEn20) +} + +func newFunction() { + _ = 3 + 4 +} + diff --git a/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_issue_44813.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_issue_44813.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_redefine.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_redefine.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_redefine.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_redefine.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_redefine.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_redefine.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_redefine.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_redefine.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_basic.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_basic.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_basic.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_basic.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_basic_nonnested.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_complex.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_complex.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_complex.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_complex.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_complex_nonnested.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_func_lit_nonnested.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_init.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_init.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_init.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_init.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_init.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_init.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_init.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_init.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_return_init_nonnested.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_scope.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_scope.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_scope.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_scope.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_scope.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_scope.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_scope.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_scope.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_smart_initialization.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_smart_return.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_smart_return.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_smart_return.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_smart_return.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_smart_return.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_smart_return.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_smart_return.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_smart_return.go.golden diff --git a/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go b/gopls/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go rename to gopls/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go diff --git a/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden b/gopls/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden similarity index 100% rename from internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden rename to gopls/internal/lsp/testdata/extract/extract_function/extract_unnecessary_param.go.golden diff --git a/internal/lsp/testdata/extract/extract_method/extract_basic.go b/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go similarity index 100% rename from internal/lsp/testdata/extract/extract_method/extract_basic.go rename to gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go diff --git a/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden b/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden new file mode 100644 index 00000000000..3310d973e01 --- /dev/null +++ b/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden @@ -0,0 +1,364 @@ +-- functionextraction_extract_basic_13_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func newFunction(a *A) int { + sum := a.x + a.y + return sum +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +-- functionextraction_extract_basic_14_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func newFunction(sum int) int { + return sum +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +-- functionextraction_extract_basic_18_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) XLessThanY() bool { + return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func newFunction(a A) bool { + return a.x < a.y +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +-- functionextraction_extract_basic_22_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func newFunction(a A) int { + sum := a.x + a.y + return sum +} + +-- functionextraction_extract_basic_23_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func newFunction(sum int) int { + return sum +} + +-- functionextraction_extract_basic_9_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func newFunction(a *A) bool { + return a.x < a.y +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +-- methodextraction_extract_basic_13_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a *A) newMethod() int { + sum := a.x + a.y + return sum +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +-- methodextraction_extract_basic_14_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (*A) newMethod(sum int) int { + return sum +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +-- methodextraction_extract_basic_18_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) XLessThanY() bool { + return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) newMethod() bool { + return a.x < a.y +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +-- methodextraction_extract_basic_22_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) newMethod() int { + sum := a.x + a.y + return sum +} + +-- methodextraction_extract_basic_23_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (A) newMethod(sum int) int { + return sum +} + +-- methodextraction_extract_basic_9_2 -- +package extract + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a *A) newMethod() bool { + return a.x < a.y +} + +func (a *A) AddP() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") +} + +func (a A) Add() int { + sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") + return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") +} + diff --git a/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go new file mode 100644 index 00000000000..cbb70a04cd1 --- /dev/null +++ b/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go @@ -0,0 +1,6 @@ +package extract + +func _() { + var _ = 1 + 2 //@suggestedfix("1", "refactor.extract", "") + var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract", "") +} diff --git a/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden new file mode 100644 index 00000000000..3fd9b328711 --- /dev/null +++ b/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden @@ -0,0 +1,18 @@ +-- suggestedfix_extract_basic_lit_4_10 -- +package extract + +func _() { + x := 1 + var _ = x + 2 //@suggestedfix("1", "refactor.extract", "") + var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract", "") +} + +-- suggestedfix_extract_basic_lit_5_10 -- +package extract + +func _() { + var _ = 1 + 2 //@suggestedfix("1", "refactor.extract", "") + x := 3 + 4 + var _ = x //@suggestedfix("3 + 4", "refactor.extract", "") +} + diff --git a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go similarity index 78% rename from internal/lsp/testdata/extract/extract_variable/extract_func_call.go rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go index badc010dce4..a20b45f5869 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go +++ b/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go @@ -3,7 +3,7 @@ package extract import "strconv" func _() { - x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract") + x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract") + b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") } diff --git a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden similarity index 60% rename from internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden index 74df67ee65f..d59c0ee99f2 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden +++ b/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden @@ -1,15 +1,3 @@ --- suggestedfix_extract_func_call_6_7 -- -package extract - -import "strconv" - -func _() { - x0 := append([]int{}, 1) - a := x0 //@suggestedfix("append([]int{}, 1)", "refactor.extract") - str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract") -} - -- suggestedfix_extract_func_call_6_8 -- package extract @@ -17,9 +5,9 @@ import "strconv" func _() { x := append([]int{}, 1) - x0 := x //@suggestedfix("append([]int{}, 1)", "refactor.extract") + x0 := x //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract") + b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") } -- suggestedfix_extract_func_call_8_12 -- @@ -28,9 +16,9 @@ package extract import "strconv" func _() { - x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract") + x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") str := "1" x, x1 := strconv.Atoi(str) - b, err := x, x1 //@suggestedfix("strconv.Atoi(str)", "refactor.extract") + b, err := x, x1 //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") } diff --git a/internal/lsp/testdata/extract/extract_variable/extract_scope.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go similarity index 62% rename from internal/lsp/testdata/extract/extract_variable/extract_scope.go rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go index 5dfcc36203b..c14ad709212 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_scope.go +++ b/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go @@ -5,9 +5,9 @@ import "go/ast" func _() { x0 := 0 if true { - y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract") + y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") } if true { - x1 := !false //@suggestedfix("!false", "refactor.extract") + x1 := !false //@suggestedfix("!false", "refactor.extract", "") } } diff --git a/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden similarity index 72% rename from internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden rename to gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden index e0e6464b59a..1c2f64b7df7 100644 --- a/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden +++ b/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden @@ -6,11 +6,11 @@ import "go/ast" func _() { x0 := 0 if true { - y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract") + y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") } if true { x := !false - x1 := x //@suggestedfix("!false", "refactor.extract") + x1 := x //@suggestedfix("!false", "refactor.extract", "") } } @@ -23,10 +23,10 @@ func _() { x0 := 0 if true { x := ast.CompositeLit{} - y := x //@suggestedfix("ast.CompositeLit{}", "refactor.extract") + y := x //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") } if true { - x1 := !false //@suggestedfix("!false", "refactor.extract") + x1 := !false //@suggestedfix("!false", "refactor.extract", "") } } diff --git a/internal/lsp/testdata/fieldlist/field_list.go b/gopls/internal/lsp/testdata/fieldlist/field_list.go similarity index 100% rename from internal/lsp/testdata/fieldlist/field_list.go rename to gopls/internal/lsp/testdata/fieldlist/field_list.go diff --git a/gopls/internal/lsp/testdata/fillstruct/a.go b/gopls/internal/lsp/testdata/fillstruct/a.go new file mode 100644 index 00000000000..e1add2d4713 --- /dev/null +++ b/gopls/internal/lsp/testdata/fillstruct/a.go @@ -0,0 +1,27 @@ +package fillstruct + +import ( + "golang.org/lsptests/fillstruct/data" +) + +type basicStruct struct { + foo int +} + +var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStruct struct { + foo int + bar string +} + +var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStruct struct { + bar string + basic basicStruct +} + +var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/gopls/internal/lsp/testdata/fillstruct/a.go.golden b/gopls/internal/lsp/testdata/fillstruct/a.go.golden new file mode 100644 index 00000000000..ca1db04ead8 --- /dev/null +++ b/gopls/internal/lsp/testdata/fillstruct/a.go.golden @@ -0,0 +1,126 @@ +-- suggestedfix_a_11_21 -- +package fillstruct + +import ( + "golang.org/lsptests/fillstruct/data" +) + +type basicStruct struct { + foo int +} + +var _ = basicStruct{ + foo: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStruct struct { + foo int + bar string +} + +var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStruct struct { + bar string + basic basicStruct +} + +var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +-- suggestedfix_a_18_22 -- +package fillstruct + +import ( + "golang.org/lsptests/fillstruct/data" +) + +type basicStruct struct { + foo int +} + +var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStruct struct { + foo int + bar string +} + +var _ = twoArgStruct{ + foo: 0, + bar: "", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStruct struct { + bar string + basic basicStruct +} + +var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +-- suggestedfix_a_25_22 -- +package fillstruct + +import ( + "golang.org/lsptests/fillstruct/data" +) + +type basicStruct struct { + foo int +} + +var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStruct struct { + foo int + bar string +} + +var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStruct struct { + bar string + basic basicStruct +} + +var _ = nestedStruct{ + bar: "", + basic: basicStruct{}, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +-- suggestedfix_a_27_16 -- +package fillstruct + +import ( + "golang.org/lsptests/fillstruct/data" +) + +type basicStruct struct { + foo int +} + +var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStruct struct { + foo int + bar string +} + +var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStruct struct { + bar string + basic basicStruct +} + +var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = data.B{ + ExportedInt: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + diff --git a/internal/lsp/testdata/fillstruct/a2.go b/gopls/internal/lsp/testdata/fillstruct/a2.go similarity index 72% rename from internal/lsp/testdata/fillstruct/a2.go rename to gopls/internal/lsp/testdata/fillstruct/a2.go index 8e12a6b54ba..b5e30a84f1e 100644 --- a/internal/lsp/testdata/fillstruct/a2.go +++ b/gopls/internal/lsp/testdata/fillstruct/a2.go @@ -8,22 +8,22 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a2.go.golden b/gopls/internal/lsp/testdata/fillstruct/a2.go.golden similarity index 73% rename from internal/lsp/testdata/fillstruct/a2.go.golden rename to gopls/internal/lsp/testdata/fillstruct/a2.go.golden index 78a6ee2b691..2eca3e349a1 100644 --- a/internal/lsp/testdata/fillstruct/a2.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/a2.go.golden @@ -15,25 +15,25 @@ var _ = typedStruct{ c: make(chan int), c1: make(<-chan int), a: [2]string{}, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a2_17_19 -- package fillstruct @@ -46,7 +46,7 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int @@ -55,19 +55,19 @@ type funStruct struct { var _ = funStruct{ fn: func(i int) int { }, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a2_23_25 -- package fillstruct @@ -80,13 +80,13 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) @@ -95,13 +95,13 @@ type funStructCompex struct { var _ = funStructCompex{ fn: func(i int, s string) (string, int) { }, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a2_29_24 -- package fillstruct @@ -114,19 +114,19 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructCompex struct { fn func(i int, s string) (string, int) } -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite") +var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") type funStructEmpty struct { fn func() @@ -135,5 +135,5 @@ type funStructEmpty struct { var _ = funStructEmpty{ fn: func() { }, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a3.go b/gopls/internal/lsp/testdata/fillstruct/a3.go similarity index 79% rename from internal/lsp/testdata/fillstruct/a3.go rename to gopls/internal/lsp/testdata/fillstruct/a3.go index 730db305423..59cd9fa28b5 100644 --- a/internal/lsp/testdata/fillstruct/a3.go +++ b/gopls/internal/lsp/testdata/fillstruct/a3.go @@ -14,7 +14,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -25,7 +25,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -33,10 +33,10 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a3.go.golden b/gopls/internal/lsp/testdata/fillstruct/a3.go.golden similarity index 79% rename from internal/lsp/testdata/fillstruct/a3.go.golden rename to gopls/internal/lsp/testdata/fillstruct/a3.go.golden index 1d8672927d9..a7c7baa8d27 100644 --- a/internal/lsp/testdata/fillstruct/a3.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/a3.go.golden @@ -18,7 +18,7 @@ type Bar struct { var _ = Bar{ X: &Foo{}, Y: &Foo{}, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -29,7 +29,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -37,13 +37,13 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a3_28_24 -- package fillstruct @@ -62,7 +62,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -81,7 +81,7 @@ var _ = importedStruct{ fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { }, st: ast.CompositeLit{}, -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -89,13 +89,13 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a3_36_30 -- package fillstruct @@ -114,7 +114,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -125,7 +125,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -137,13 +137,13 @@ var _ = pointerBuiltinStruct{ b: new(bool), s: new(string), i: new(int), -} //@suggestedfix("}", "refactor.rewrite") +} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a3_39_3 -- package fillstruct @@ -162,7 +162,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -173,7 +173,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -181,17 +181,17 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ { ValuePos: 0, Kind: 0, Value: "", - }, //@suggestedfix("}", "refactor.rewrite") + }, //@suggestedfix("}", "refactor.rewrite", "Fill") } -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite") +var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") -- suggestedfix_a3_42_25 -- package fillstruct @@ -210,7 +210,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite") +var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -221,7 +221,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") type pointerBuiltinStruct struct { b *bool @@ -229,15 +229,15 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite") +var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite") + {}, //@suggestedfix("}", "refactor.rewrite", "Fill") } var _ = []ast.BasicLit{{ ValuePos: 0, Kind: 0, Value: "", -}} //@suggestedfix("}", "refactor.rewrite") +}} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/internal/lsp/testdata/fillstruct/a4.go b/gopls/internal/lsp/testdata/fillstruct/a4.go similarity index 55% rename from internal/lsp/testdata/fillstruct/a4.go rename to gopls/internal/lsp/testdata/fillstruct/a4.go index 7833d338c64..5f52a55fa72 100644 --- a/internal/lsp/testdata/fillstruct/a4.go +++ b/gopls/internal/lsp/testdata/fillstruct/a4.go @@ -22,18 +22,18 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite") + var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/a4.go.golden b/gopls/internal/lsp/testdata/fillstruct/a4.go.golden similarity index 61% rename from internal/lsp/testdata/fillstruct/a4.go.golden rename to gopls/internal/lsp/testdata/fillstruct/a4.go.golden index 109c6b5ea47..b1e376f05f1 100644 --- a/internal/lsp/testdata/fillstruct/a4.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/a4.go.golden @@ -25,20 +25,20 @@ func fill() { var x int var _ = iStruct{ X: x, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite") + var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_a4_28_18 -- @@ -66,22 +66,22 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var s string var _ = sStruct{ str: s, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite") + var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_a4_35_20 -- @@ -109,10 +109,10 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} @@ -123,10 +123,10 @@ func fill() { num: n, strin: s, arr: []int{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_a4_38_23 -- @@ -154,21 +154,21 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite") + var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite") + var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") var node *ast.CompositeLit var _ = assignStruct{ n: node, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/data/a.go b/gopls/internal/lsp/testdata/fillstruct/data/a.go similarity index 100% rename from internal/lsp/testdata/fillstruct/data/a.go rename to gopls/internal/lsp/testdata/fillstruct/data/a.go diff --git a/internal/lsp/testdata/fillstruct/fill_struct.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct.go similarity index 50% rename from internal/lsp/testdata/fillstruct/fill_struct.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct.go index fccec135321..3da904741d0 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct.go +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct.go @@ -17,10 +17,10 @@ type StructA3 struct { } func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite") + a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") + b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") + c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite") + _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") } } diff --git a/internal/lsp/testdata/fillstruct/fill_struct.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden similarity index 59% rename from internal/lsp/testdata/fillstruct/fill_struct.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden index 8d997031516..de01a40f052 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden @@ -24,11 +24,11 @@ func fill() { MapA: map[int]string{}, Array: []int{}, StructB: StructB{}, - } //@suggestedfix("}", "refactor.rewrite") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") + b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") + c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite") + _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") } } @@ -52,13 +52,13 @@ type StructA3 struct { } func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite") + a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") b := StructA2{ B: &StructB{}, - } //@suggestedfix("}", "refactor.rewrite") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") + c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite") + _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") } } @@ -82,13 +82,13 @@ type StructA3 struct { } func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite") + a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") + b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") c := StructA3{ B: StructB{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite") + _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") } } @@ -112,13 +112,13 @@ type StructA3 struct { } func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite") + a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") + b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") + c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") if true { _ = StructA3{ B: StructB{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_anon.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go similarity index 68% rename from internal/lsp/testdata/fillstruct/fill_struct_anon.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go index b5d2337fd9d..2c099a80ea7 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_anon.go +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go @@ -10,5 +10,5 @@ type StructAnon struct { } func fill() { - _ := StructAnon{} //@suggestedfix("}", "refactor.rewrite") + _ := StructAnon{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden similarity index 85% rename from internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden index eb6ffd66136..7cc9ac23d02 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden @@ -15,6 +15,6 @@ func fill() { a: struct{}{}, b: map[string]interface{}{}, c: map[string]struct{d int; e bool}{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_nested.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go similarity index 65% rename from internal/lsp/testdata/fillstruct/fill_struct_nested.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go index 79eb84b7478..ab7be5a7b58 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_nested.go +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go @@ -10,6 +10,6 @@ type StructC struct { func nested() { c := StructB{ - StructC: StructC{}, //@suggestedfix("}", "refactor.rewrite") + StructC: StructC{}, //@suggestedfix("}", "refactor.rewrite", "Fill") } } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden similarity index 80% rename from internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden index 30061a5d72a..c902ee7f12b 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden @@ -13,7 +13,7 @@ func nested() { c := StructB{ StructC: StructC{ unexportedInt: 0, - }, //@suggestedfix("}", "refactor.rewrite") + }, //@suggestedfix("}", "refactor.rewrite", "Fill") } } diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go new file mode 100644 index 00000000000..ef35627c8ea --- /dev/null +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go @@ -0,0 +1,12 @@ +package fillstruct + +import ( + h2 "net/http" + + "golang.org/lsptests/fillstruct/data" +) + +func unexported() { + a := data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") + _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite", "Fill") +} diff --git a/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden similarity index 55% rename from internal/lsp/testdata/fillstruct/fill_struct_package.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden index 13c85702527..0cdbfc820ba 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden @@ -4,14 +4,14 @@ package fillstruct import ( h2 "net/http" - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) func unexported() { a := data.B{ ExportedInt: 0, - } //@suggestedfix("}", "refactor.rewrite") - _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") + _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_fill_struct_package_11_16 -- @@ -20,17 +20,17 @@ package fillstruct import ( h2 "net/http" - "golang.org/x/tools/internal/lsp/fillstruct/data" + "golang.org/lsptests/fillstruct/data" ) func unexported() { - a := data.B{} //@suggestedfix("}", "refactor.rewrite") + a := data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") _ = h2.Client{ Transport: nil, CheckRedirect: func(req *h2.Request, via []*h2.Request) error { }, Jar: nil, Timeout: 0, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_partial.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go similarity index 78% rename from internal/lsp/testdata/fillstruct/fill_struct_partial.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go index 97b517dcdc3..5de1722c783 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_partial.go +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go @@ -14,11 +14,11 @@ type StructPartialB struct { func fill() { a := StructPartialA{ PrefilledInt: 5, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") b := StructPartialB{ /* this comment should disappear */ PrefilledInt: 7, // This comment should be blown away. /* As should this one */ - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden similarity index 79% rename from internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden index 2d063c14d39..3aa437a0334 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden @@ -17,13 +17,13 @@ func fill() { PrefilledInt: 5, UnfilledInt: 0, StructPartialB: StructPartialB{}, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") b := StructPartialB{ /* this comment should disappear */ PrefilledInt: 7, // This comment should be blown away. /* As should this one */ - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } -- suggestedfix_fill_struct_partial_23_2 -- @@ -43,10 +43,10 @@ type StructPartialB struct { func fill() { a := StructPartialA{ PrefilledInt: 5, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") b := StructPartialB{ PrefilledInt: 7, UnfilledInt: 0, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go similarity index 56% rename from internal/lsp/testdata/fillstruct/fill_struct_spaces.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go index d5d1bbba5c3..6a468cd544c 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go @@ -5,5 +5,5 @@ type StructD struct { } func spaces() { - d := StructD{} //@suggestedfix("}", "refactor.rewrite") + d := StructD{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden similarity index 76% rename from internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden index 0d755334c99..590c91611d0 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden @@ -8,6 +8,6 @@ type StructD struct { func spaces() { d := StructD{ ExportedIntField: 0, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go similarity index 60% rename from internal/lsp/testdata/fillstruct/fill_struct_unsafe.go rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go index 50877e9005c..f5e42a4f2fe 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go @@ -8,5 +8,5 @@ type unsafeStruct struct { } func fill() { - _ := unsafeStruct{} //@suggestedfix("}", "refactor.rewrite") + _ := unsafeStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden similarity index 78% rename from internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden rename to gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden index 99369544373..7e8e1952f86 100644 --- a/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden +++ b/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden @@ -12,6 +12,6 @@ func fill() { _ := unsafeStruct{ x: 0, p: nil, - } //@suggestedfix("}", "refactor.rewrite") + } //@suggestedfix("}", "refactor.rewrite", "Fill") } diff --git a/gopls/internal/lsp/testdata/fillstruct/typeparams.go b/gopls/internal/lsp/testdata/fillstruct/typeparams.go new file mode 100644 index 00000000000..c0b702f57c7 --- /dev/null +++ b/gopls/internal/lsp/testdata/fillstruct/typeparams.go @@ -0,0 +1,37 @@ +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +func _[T any]() { + type S struct{ t T } + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") +} diff --git a/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden b/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden new file mode 100644 index 00000000000..625df7577b7 --- /dev/null +++ b/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden @@ -0,0 +1,206 @@ +-- suggestedfix_typeparams_14_40 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{ + foo: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +func _[T any]() { + type S struct{ t T } + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") +} + +-- suggestedfix_typeparams_21_49 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{ + foo: "", + bar: 0, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +func _[T any]() { + type S struct{ t T } + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") +} + +-- suggestedfix_typeparams_25_1 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + foo: 0, + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +func _[T any]() { + type S struct{ t T } + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") +} + +-- suggestedfix_typeparams_32_36 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{ + bar: "", + basic: basicStructWithTypeParams{}, +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +func _[T any]() { + type S struct{ t T } + _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") +} + +-- suggestedfix_typeparams_36_8 -- +//go:build go1.18 +// +build go1.18 + +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@suggestedfix("}", "refactor.rewrite", "Fill") + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") + +func _[T any]() { + type S struct{ t T } + _ = S{ + t: *new(T), + } //@suggestedfix("}", "refactor.rewrite", "Fill") +} + diff --git a/internal/lsp/testdata/folding/a.go b/gopls/internal/lsp/testdata/folding/a.go similarity index 100% rename from internal/lsp/testdata/folding/a.go rename to gopls/internal/lsp/testdata/folding/a.go diff --git a/internal/lsp/testdata/folding/a.go.golden b/gopls/internal/lsp/testdata/folding/a.go.golden similarity index 100% rename from internal/lsp/testdata/folding/a.go.golden rename to gopls/internal/lsp/testdata/folding/a.go.golden diff --git a/internal/lsp/testdata/folding/bad.go.golden b/gopls/internal/lsp/testdata/folding/bad.go.golden similarity index 100% rename from internal/lsp/testdata/folding/bad.go.golden rename to gopls/internal/lsp/testdata/folding/bad.go.golden diff --git a/internal/lsp/testdata/folding/bad.go.in b/gopls/internal/lsp/testdata/folding/bad.go.in similarity index 100% rename from internal/lsp/testdata/folding/bad.go.in rename to gopls/internal/lsp/testdata/folding/bad.go.in diff --git a/internal/lsp/testdata/foo/foo.go b/gopls/internal/lsp/testdata/foo/foo.go similarity index 94% rename from internal/lsp/testdata/foo/foo.go rename to gopls/internal/lsp/testdata/foo/foo.go index 20ea183e5d9..66631c58ca9 100644 --- a/internal/lsp/testdata/foo/foo.go +++ b/gopls/internal/lsp/testdata/foo/foo.go @@ -1,4 +1,4 @@ -package foo //@mark(PackageFoo, "foo"),item(PackageFoo, "foo", "\"golang.org/x/tools/internal/lsp/foo\"", "package") +package foo //@mark(PackageFoo, "foo"),item(PackageFoo, "foo", "\"golang.org/lsptests/foo\"", "package") type StructFoo struct { //@item(StructFoo, "StructFoo", "struct{...}", "struct") Value int //@item(Value, "Value", "int", "field") diff --git a/internal/lsp/testdata/format/bad_format.go.golden b/gopls/internal/lsp/testdata/format/bad_format.go.golden similarity index 68% rename from internal/lsp/testdata/format/bad_format.go.golden rename to gopls/internal/lsp/testdata/format/bad_format.go.golden index c2ac5a1a13e..f0c24d6356e 100644 --- a/internal/lsp/testdata/format/bad_format.go.golden +++ b/gopls/internal/lsp/testdata/format/bad_format.go.golden @@ -9,7 +9,7 @@ import ( func hello() { - var x int //@diag("x", "compiler", "x declared but not used", "error") + var x int //@diag("x", "compiler", "x declared (and|but) not used", "error") } func hi() { diff --git a/internal/lsp/testdata/format/bad_format.go.in b/gopls/internal/lsp/testdata/format/bad_format.go.in similarity index 67% rename from internal/lsp/testdata/format/bad_format.go.in rename to gopls/internal/lsp/testdata/format/bad_format.go.in index 06187238ebe..995ec399a11 100644 --- a/internal/lsp/testdata/format/bad_format.go.in +++ b/gopls/internal/lsp/testdata/format/bad_format.go.in @@ -11,7 +11,7 @@ func hello() { - var x int //@diag("x", "compiler", "x declared but not used", "error") + var x int //@diag("x", "compiler", "x declared (and|but) not used", "error") } func hi() { diff --git a/internal/lsp/testdata/format/good_format.go b/gopls/internal/lsp/testdata/format/good_format.go similarity index 100% rename from internal/lsp/testdata/format/good_format.go rename to gopls/internal/lsp/testdata/format/good_format.go diff --git a/internal/lsp/testdata/format/good_format.go.golden b/gopls/internal/lsp/testdata/format/good_format.go.golden similarity index 100% rename from internal/lsp/testdata/format/good_format.go.golden rename to gopls/internal/lsp/testdata/format/good_format.go.golden diff --git a/internal/lsp/testdata/format/newline_format.go.golden b/gopls/internal/lsp/testdata/format/newline_format.go.golden similarity index 100% rename from internal/lsp/testdata/format/newline_format.go.golden rename to gopls/internal/lsp/testdata/format/newline_format.go.golden diff --git a/internal/lsp/testdata/format/newline_format.go.in b/gopls/internal/lsp/testdata/format/newline_format.go.in similarity index 100% rename from internal/lsp/testdata/format/newline_format.go.in rename to gopls/internal/lsp/testdata/format/newline_format.go.in diff --git a/internal/lsp/testdata/format/one_line.go.golden b/gopls/internal/lsp/testdata/format/one_line.go.golden similarity index 100% rename from internal/lsp/testdata/format/one_line.go.golden rename to gopls/internal/lsp/testdata/format/one_line.go.golden diff --git a/internal/lsp/testdata/format/one_line.go.in b/gopls/internal/lsp/testdata/format/one_line.go.in similarity index 100% rename from internal/lsp/testdata/format/one_line.go.in rename to gopls/internal/lsp/testdata/format/one_line.go.in diff --git a/internal/lsp/testdata/func_rank/func_rank.go.in b/gopls/internal/lsp/testdata/func_rank/func_rank.go.in similarity index 100% rename from internal/lsp/testdata/func_rank/func_rank.go.in rename to gopls/internal/lsp/testdata/func_rank/func_rank.go.in diff --git a/internal/lsp/testdata/funcsig/func_sig.go b/gopls/internal/lsp/testdata/funcsig/func_sig.go similarity index 100% rename from internal/lsp/testdata/funcsig/func_sig.go rename to gopls/internal/lsp/testdata/funcsig/func_sig.go diff --git a/internal/lsp/testdata/funcvalue/func_value.go b/gopls/internal/lsp/testdata/funcvalue/func_value.go similarity index 100% rename from internal/lsp/testdata/funcvalue/func_value.go rename to gopls/internal/lsp/testdata/funcvalue/func_value.go diff --git a/internal/lsp/testdata/fuzzymatch/fuzzymatch.go b/gopls/internal/lsp/testdata/fuzzymatch/fuzzymatch.go similarity index 100% rename from internal/lsp/testdata/fuzzymatch/fuzzymatch.go rename to gopls/internal/lsp/testdata/fuzzymatch/fuzzymatch.go diff --git a/internal/lsp/testdata/generate/generate.go b/gopls/internal/lsp/testdata/generate/generate.go similarity index 100% rename from internal/lsp/testdata/generate/generate.go rename to gopls/internal/lsp/testdata/generate/generate.go diff --git a/internal/lsp/testdata/generated/generated.go b/gopls/internal/lsp/testdata/generated/generated.go similarity index 50% rename from internal/lsp/testdata/generated/generated.go rename to gopls/internal/lsp/testdata/generated/generated.go index c92bd9eb8c3..c7adc180409 100644 --- a/internal/lsp/testdata/generated/generated.go +++ b/gopls/internal/lsp/testdata/generated/generated.go @@ -3,5 +3,5 @@ package generated // Code generated by generator.go. DO NOT EDIT. func _() { - var y int //@diag("y", "compiler", "y declared but not used", "error") + var y int //@diag("y", "compiler", "y declared (and|but) not used", "error") } diff --git a/gopls/internal/lsp/testdata/generated/generator.go b/gopls/internal/lsp/testdata/generated/generator.go new file mode 100644 index 00000000000..8e2a4fab722 --- /dev/null +++ b/gopls/internal/lsp/testdata/generated/generator.go @@ -0,0 +1,5 @@ +package generated + +func _() { + var x int //@diag("x", "compiler", "x declared (and|but) not used", "error") +} diff --git a/internal/lsp/testdata/godef/a/a.go b/gopls/internal/lsp/testdata/godef/a/a.go similarity index 92% rename from internal/lsp/testdata/godef/a/a.go rename to gopls/internal/lsp/testdata/godef/a/a.go index 5cc85527aeb..53ca6ddc412 100644 --- a/internal/lsp/testdata/godef/a/a.go +++ b/gopls/internal/lsp/testdata/godef/a/a.go @@ -60,16 +60,16 @@ type R struct { func (_ R) Hey() {} //@mark(AHey, "Hey") -type H interface { +type H interface { //@H Goodbye() //@mark(AGoodbye, "Goodbye") } -type I interface { +type I interface { //@I B() //@mark(AB, "B") J } -type J interface { +type J interface { //@J Hello() //@mark(AHello, "Hello") } @@ -103,3 +103,9 @@ func _() { } // e has a comment ) } + +var ( + hh H //@hoverdef("H", H) + ii I //@hoverdef("I", I) + jj J //@hoverdef("J", J) +) diff --git a/internal/lsp/testdata/godef/a/a.go.golden b/gopls/internal/lsp/testdata/godef/a/a.go.golden similarity index 55% rename from internal/lsp/testdata/godef/a/a.go.golden rename to gopls/internal/lsp/testdata/godef/a/a.go.golden index 9f67a147d14..470396d068c 100644 --- a/internal/lsp/testdata/godef/a/a.go.golden +++ b/gopls/internal/lsp/testdata/godef/a/a.go.golden @@ -1,25 +1,52 @@ +-- H-hoverdef -- +```go +type H interface { + Goodbye() //@mark(AGoodbye, "Goodbye") +} +``` + +[`a.H` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#H) +-- I-hoverdef -- +```go +type I interface { + B() //@mark(AB, "B") + J +} +``` + +[`a.I` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#I) +-- J-hoverdef -- +```go +type J interface { + Hello() //@mark(AHello, "Hello") +} +``` + +[`a.J` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#J) -- Lock-hoverdef -- ```go func (*sync.Mutex).Lock() ``` -Lock locks m\. +Lock locks m. + -[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync?utm_source=gopls#Mutex.Lock) +[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock) -- Name-hoverdef -- ```go func (*types.object).Name() string ``` -Name returns the object\'s \(package\-local, unqualified\) name\. +Name returns the object's (package-local, unqualified) name. + -[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types?utm_source=gopls#TypeName.Name) +[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name) -- Random-definition -- godef/a/random.go:3:6-12: defined here as ```go func Random() int ``` -[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random) +[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random) -- Random-definition-json -- { "span": { @@ -35,7 +62,7 @@ func Random() int "offset": 22 } }, - "description": "```go\nfunc Random() int\n```\n\n[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random)" + "description": "```go\nfunc Random() int\n```\n\n[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random)" } -- Random-hoverdef -- @@ -43,13 +70,13 @@ func Random() int func Random() int ``` -[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random) +[`a.Random` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random) -- Random2-definition -- godef/a/random.go:8:6-13: defined here as ```go func Random2(y int) int ``` -[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random2) +[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2) -- Random2-definition-json -- { "span": { @@ -65,7 +92,7 @@ func Random2(y int) int "offset": 78 } }, - "description": "```go\nfunc Random2(y int) int\n```\n\n[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random2)" + "description": "```go\nfunc Random2(y int) int\n```\n\n[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2)" } -- Random2-hoverdef -- @@ -73,9 +100,10 @@ func Random2(y int) int func Random2(y int) int ``` -[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random2) +[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Random2) -- aPackage-hoverdef -- -Package a is a package for testing go to definition\. +Package a is a package for testing go to definition. + -- declBlockA-hoverdef -- ```go type a struct { @@ -84,12 +112,14 @@ type a struct { ``` 1st type declaration block + -- declBlockB-hoverdef -- ```go type b struct{} ``` b has a comment + -- declBlockC-hoverdef -- ```go type c struct { @@ -98,12 +128,14 @@ type c struct { ``` c is a struct + -- declBlockD-hoverdef -- ```go type d string ``` 3rd type declaration block + -- declBlockE-hoverdef -- ```go type e struct { @@ -112,12 +144,13 @@ type e struct { ``` e has a comment + -- err-definition -- godef/a/a.go:33:6-9: defined here as ```go var err error ``` -\@err +@err -- err-definition-json -- { "span": { @@ -133,7 +166,7 @@ var err error "offset": 615 } }, - "description": "```go\nvar err error\n```\n\n\\@err" + "description": "```go\nvar err error\n```\n\n@err" } -- err-hoverdef -- @@ -141,50 +174,57 @@ var err error var err error ``` -\@err +@err + -- g-hoverdef -- ```go const g untyped int = 1 ``` -When I hover on g, I should see this comment\. +When I hover on g, I should see this comment. + -- h-hoverdef -- ```go const h untyped int = 2 ``` -Constant block\. +Constant block. + -- make-hoverdef -- ```go func make(t Type, size ...int) Type ``` -The make built\-in function allocates and initializes an object of type slice, map, or chan \(only\)\. +The make built-in function allocates and initializes an object of type slice, map, or chan (only). + -[`make` on pkg.go.dev](https://pkg.go.dev/builtin?utm_source=gopls#make) +[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make) -- string-hoverdef -- ```go type string string ``` -string is the set of all strings of 8\-bit bytes, conventionally but not necessarily representing UTF\-8\-encoded text\. +string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. -[`string` on pkg.go.dev](https://pkg.go.dev/builtin?utm_source=gopls#string) + +[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string) -- typesImport-hoverdef -- ```go package types ("go/types") ``` -[`types` on pkg.go.dev](https://pkg.go.dev/go/types?utm_source=gopls) +[`types` on pkg.go.dev](https://pkg.go.dev/go/types) -- x-hoverdef -- ```go var x string ``` -x is a variable\. +x is a variable. + -- z-hoverdef -- ```go var z string ``` -z is a variable too\. +z is a variable too. + diff --git a/internal/lsp/testdata/godef/a/a_test.go b/gopls/internal/lsp/testdata/godef/a/a_test.go similarity index 100% rename from internal/lsp/testdata/godef/a/a_test.go rename to gopls/internal/lsp/testdata/godef/a/a_test.go diff --git a/internal/lsp/testdata/godef/a/a_test.go.golden b/gopls/internal/lsp/testdata/godef/a/a_test.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/a_test.go.golden rename to gopls/internal/lsp/testdata/godef/a/a_test.go.golden diff --git a/gopls/internal/lsp/testdata/godef/a/a_x_test.go b/gopls/internal/lsp/testdata/godef/a/a_x_test.go new file mode 100644 index 00000000000..f166f055084 --- /dev/null +++ b/gopls/internal/lsp/testdata/godef/a/a_x_test.go @@ -0,0 +1,9 @@ +package a_test + +import ( + "testing" +) + +func TestA2(t *testing.T) { //@TestA2,godef(TestA2, TestA2) + Nonexistant() //@diag("Nonexistant", "compiler", "(undeclared name|undefined): Nonexistant", "error") +} diff --git a/internal/lsp/testdata/godef/a/a_x_test.go.golden b/gopls/internal/lsp/testdata/godef/a/a_x_test.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/a_x_test.go.golden rename to gopls/internal/lsp/testdata/godef/a/a_x_test.go.golden diff --git a/internal/lsp/testdata/godef/a/d.go b/gopls/internal/lsp/testdata/godef/a/d.go similarity index 65% rename from internal/lsp/testdata/godef/a/d.go rename to gopls/internal/lsp/testdata/godef/a/d.go index 2da8d058edf..a1d17ad0da3 100644 --- a/internal/lsp/testdata/godef/a/d.go +++ b/gopls/internal/lsp/testdata/godef/a/d.go @@ -16,6 +16,16 @@ func (t Thing) Method(i int) string { //@Method return t.Member } +func (t Thing) Method3() { +} + +func (t *Thing) Method2(i int, j int) (error, string) { + return nil, t.Member +} + +func (t *Thing) private() { +} + func useThings() { t := Thing{ //@mark(aStructType, "ing") Member: "string", //@mark(fMember, "ember") @@ -26,6 +36,22 @@ func useThings() { t.Method() //@mark(aMethod, "eth") } +type NextThing struct { //@NextThing + Thing + Value int +} + +func (n NextThing) another() string { + return n.Member +} + +// Shadows Thing.Method3 +func (n *NextThing) Method3() int { + return n.Value +} + +var nextThing NextThing //@hoverdef("NextThing", NextThing) + /*@ godef(aStructType, Thing) godef(aMember, Member) diff --git a/internal/lsp/testdata/godef/a/d.go.golden b/gopls/internal/lsp/testdata/godef/a/d.go.golden similarity index 57% rename from internal/lsp/testdata/godef/a/d.go.golden rename to gopls/internal/lsp/testdata/godef/a/d.go.golden index 47723b0453c..ee687750c3e 100644 --- a/internal/lsp/testdata/godef/a/d.go.golden +++ b/gopls/internal/lsp/testdata/godef/a/d.go.golden @@ -3,9 +3,10 @@ godef/a/d.go:6:2-8: defined here as ```go field Member string ``` -\@Member +@Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member) + +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member) -- Member-definition-json -- { "span": { @@ -21,7 +22,7 @@ field Member string "offset": 96 } }, - "description": "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member)" + "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)" } -- Member-hoverdef -- @@ -29,15 +30,16 @@ field Member string field Member string ``` -\@Member +@Member + -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member) -- Method-definition -- godef/a/d.go:15:16-22: defined here as ```go func (Thing).Method(i int) string ``` -[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Method) +[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method) -- Method-definition-json -- { "span": { @@ -53,7 +55,7 @@ func (Thing).Method(i int) string "offset": 225 } }, - "description": "```go\nfunc (Thing).Method(i int) string\n```\n\n[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Method)" + "description": "```go\nfunc (Thing).Method(i int) string\n```\n\n[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method)" } -- Method-hoverdef -- @@ -61,15 +63,28 @@ func (Thing).Method(i int) string func (Thing).Method(i int) string ``` -[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Method) +[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Method) +-- NextThing-hoverdef -- +```go +type NextThing struct { + Thing + Value int +} + +func (*NextThing).Method3() int +func (NextThing).another() string +``` + +[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#NextThing) -- Other-definition -- godef/a/d.go:9:5-10: defined here as ```go var Other Thing ``` -\@Other +@Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other) + +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other) -- Other-definition-json -- { "span": { @@ -85,7 +100,7 @@ var Other Thing "offset": 126 } }, - "description": "```go\nvar Other Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other)" + "description": "```go\nvar Other Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)" } -- Other-hoverdef -- @@ -93,17 +108,23 @@ var Other Thing var Other Thing ``` -\@Other +@Other + -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other) -- Thing-definition -- godef/a/d.go:5:6-11: defined here as ```go type Thing struct { Member string //@Member } + +func (Thing).Method(i int) string +func (*Thing).Method2(i int, j int) (error, string) +func (Thing).Method3() +func (*Thing).private() ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) -- Thing-definition-json -- { "span": { @@ -119,7 +140,7 @@ type Thing struct { "offset": 70 } }, - "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing)" + "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n\nfunc (Thing).Method(i int) string\nfunc (*Thing).Method2(i int, j int) (error, string)\nfunc (Thing).Method3()\nfunc (*Thing).private()\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)" } -- Thing-hoverdef -- @@ -127,15 +148,20 @@ type Thing struct { type Thing struct { Member string //@Member } + +func (Thing).Method(i int) string +func (*Thing).Method2(i int, j int) (error, string) +func (Thing).Method3() +func (*Thing).private() ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) -- Things-definition -- godef/a/d.go:11:6-12: defined here as ```go func Things(val []string) []Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things) -- Things-definition-json -- { "span": { @@ -151,7 +177,7 @@ func Things(val []string) []Thing "offset": 154 } }, - "description": "```go\nfunc Things(val []string) []Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things)" + "description": "```go\nfunc Things(val []string) []Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)" } -- Things-hoverdef -- @@ -159,6 +185,7 @@ func Things(val []string) []Thing func Things(val []string) []Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things) -- a-hoverdef -- -Package a is a package for testing go to definition\. +Package a is a package for testing go to definition. + diff --git a/internal/lsp/testdata/godef/a/f.go b/gopls/internal/lsp/testdata/godef/a/f.go similarity index 100% rename from internal/lsp/testdata/godef/a/f.go rename to gopls/internal/lsp/testdata/godef/a/f.go diff --git a/internal/lsp/testdata/godef/a/f.go.golden b/gopls/internal/lsp/testdata/godef/a/f.go.golden similarity index 100% rename from internal/lsp/testdata/godef/a/f.go.golden rename to gopls/internal/lsp/testdata/godef/a/f.go.golden diff --git a/internal/lsp/testdata/godef/a/g.go b/gopls/internal/lsp/testdata/godef/a/g.go similarity index 100% rename from internal/lsp/testdata/godef/a/g.go rename to gopls/internal/lsp/testdata/godef/a/g.go diff --git a/internal/lsp/testdata/godef/a/g.go.golden b/gopls/internal/lsp/testdata/godef/a/g.go.golden similarity index 65% rename from internal/lsp/testdata/godef/a/g.go.golden rename to gopls/internal/lsp/testdata/godef/a/g.go.golden index b7ed7392806..f7a2e1b0775 100644 --- a/internal/lsp/testdata/godef/a/g.go.golden +++ b/gopls/internal/lsp/testdata/godef/a/g.go.golden @@ -3,4 +3,5 @@ const dur time.Duration = 910350000000 // 15m10.35s ``` -dur is a constant of type time\.Duration\. +dur is a constant of type time.Duration. + diff --git a/internal/lsp/testdata/godef/a/h.go b/gopls/internal/lsp/testdata/godef/a/h.go similarity index 100% rename from internal/lsp/testdata/godef/a/h.go rename to gopls/internal/lsp/testdata/godef/a/h.go diff --git a/internal/lsp/testdata/godef/a/h.go.golden b/gopls/internal/lsp/testdata/godef/a/h.go.golden similarity index 98% rename from internal/lsp/testdata/godef/a/h.go.golden rename to gopls/internal/lsp/testdata/godef/a/h.go.golden index 4b27211e9aa..295876647ab 100644 --- a/internal/lsp/testdata/godef/a/h.go.golden +++ b/gopls/internal/lsp/testdata/godef/a/h.go.golden @@ -4,42 +4,49 @@ field d int ``` d field + -- arrE-hoverdef -- ```go field e struct{f int} ``` e nested struct + -- arrF-hoverdef -- ```go field f int ``` f field of nested struct + -- complexH-hoverdef -- ```go field h int ``` h field + -- complexI-hoverdef -- ```go field i struct{j int} ``` i nested struct + -- complexJ-hoverdef -- ```go field j int ``` j field of nested struct + -- mapStructKeyX-hoverdef -- ```go field x []string ``` X key field + -- mapStructKeyY-hoverdef -- ```go field y string @@ -50,87 +57,102 @@ field x string ``` X value field + -- nestedMap-hoverdef -- ```go field m map[string]float64 ``` nested map + -- nestedNumber-hoverdef -- ```go field number int64 ``` nested number + -- nestedString-hoverdef -- ```go field str string ``` nested string + -- openMethod-hoverdef -- ```go func (interface).open() error ``` open method comment + -- returnX-hoverdef -- ```go field x int ``` X coord + -- returnY-hoverdef -- ```go field y int ``` Y coord + -- structA-hoverdef -- ```go field a int ``` a field + -- structB-hoverdef -- ```go field b struct{c int} ``` b nested struct + -- structC-hoverdef -- ```go field c int ``` c field of nested struct + -- testDescription-hoverdef -- ```go field desc string ``` test description + -- testInput-hoverdef -- ```go field in map[string][]struct{key string; value interface{}} ``` test input + -- testInputKey-hoverdef -- ```go field key string ``` test key + -- testInputValue-hoverdef -- ```go field value interface{} ``` test value + -- testResultValue-hoverdef -- ```go field value int ``` expected test value + diff --git a/internal/lsp/testdata/godef/a/random.go b/gopls/internal/lsp/testdata/godef/a/random.go similarity index 100% rename from internal/lsp/testdata/godef/a/random.go rename to gopls/internal/lsp/testdata/godef/a/random.go diff --git a/internal/lsp/testdata/godef/a/random.go.golden b/gopls/internal/lsp/testdata/godef/a/random.go.golden similarity index 75% rename from internal/lsp/testdata/godef/a/random.go.golden rename to gopls/internal/lsp/testdata/godef/a/random.go.golden index 381a11acee8..d7ba51d1e82 100644 --- a/internal/lsp/testdata/godef/a/random.go.golden +++ b/gopls/internal/lsp/testdata/godef/a/random.go.golden @@ -3,7 +3,7 @@ godef/a/random.go:24:15-18: defined here as ```go func (*Pos).Sum() int ``` -[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Pos.Sum) +[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum) -- PosSum-definition-json -- { "span": { @@ -19,7 +19,7 @@ func (*Pos).Sum() int "offset": 416 } }, - "description": "```go\nfunc (*Pos).Sum() int\n```\n\n[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Pos.Sum)" + "description": "```go\nfunc (*Pos).Sum() int\n```\n\n[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum)" } -- PosSum-hoverdef -- @@ -27,13 +27,13 @@ func (*Pos).Sum() int func (*Pos).Sum() int ``` -[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Pos.Sum) +[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Pos.Sum) -- PosX-definition -- godef/a/random.go:13:2-3: defined here as ```go field x int ``` -\@mark\(PosX, \"x\"\),mark\(PosY, \"y\"\) +@mark(PosX, "x"),mark(PosY, "y") -- PosX-definition-json -- { "span": { @@ -49,7 +49,7 @@ field x int "offset": 188 } }, - "description": "```go\nfield x int\n```\n\n\\@mark\\(PosX, \\\"x\\\"\\),mark\\(PosY, \\\"y\\\"\\)" + "description": "```go\nfield x int\n```\n\n@mark(PosX, \"x\"),mark(PosY, \"y\")" } -- PosX-hoverdef -- @@ -57,7 +57,8 @@ field x int field x int ``` -\@mark\(PosX, \"x\"\),mark\(PosY, \"y\"\) +@mark(PosX, "x"),mark(PosY, "y") + -- RandomParamY-definition -- godef/a/random.go:8:14-15: defined here as ```go var y int diff --git a/internal/lsp/testdata/godef/b/b.go b/gopls/internal/lsp/testdata/godef/b/b.go similarity index 87% rename from internal/lsp/testdata/godef/b/b.go rename to gopls/internal/lsp/testdata/godef/b/b.go index f9c1d64024b..ee536ecfdc3 100644 --- a/internal/lsp/testdata/godef/b/b.go +++ b/gopls/internal/lsp/testdata/godef/b/b.go @@ -1,8 +1,8 @@ package b import ( - myFoo "golang.org/x/tools/internal/lsp/foo" //@mark(myFoo, "myFoo"),godef("myFoo", myFoo) - "golang.org/x/tools/internal/lsp/godef/a" //@mark(AImport, re"\".*\"") + myFoo "golang.org/lsptests/foo" //@mark(myFoo, "myFoo"),godef("myFoo", myFoo) + "golang.org/lsptests/godef/a" //@mark(AImport, re"\".*\"") ) type Embed struct { diff --git a/gopls/internal/lsp/testdata/godef/b/b.go.golden b/gopls/internal/lsp/testdata/godef/b/b.go.golden new file mode 100644 index 00000000000..cfe3917ba88 --- /dev/null +++ b/gopls/internal/lsp/testdata/godef/b/b.go.golden @@ -0,0 +1,480 @@ +-- AB-hoverdef -- +```go +func (a.I).B() +``` + +@mark(AB, "B") + + +[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#I.B) +-- AField-hoverdef -- +```go +field Field int +``` + +@mark(AField, "Field") + + +[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#S.Field) +-- AField2-hoverdef -- +```go +field Field2 int +``` + +@mark(AField2, "Field2") + + +[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#R.Field2) +-- AGoodbye-hoverdef -- +```go +func (a.H).Goodbye() +``` + +@mark(AGoodbye, "Goodbye") + + +[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#H.Goodbye) +-- AHello-hoverdef -- +```go +func (a.J).Hello() +``` + +@mark(AHello, "Hello") + + +[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#J.Hello) +-- AHey-hoverdef -- +```go +func (a.R).Hey() +``` + +[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#R.Hey) +-- AHi-hoverdef -- +```go +func (a.A).Hi() +``` + +[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A.Hi) +-- AImport-definition -- +godef/b/b.go:5:2-31: defined here as ```go +package a ("golang.org/lsptests/godef/a") +``` + +[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a) +-- AImport-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 5, + "column": 2, + "offset": 100 + }, + "end": { + "line": 5, + "column": 31, + "offset": 129 + } + }, + "description": "```go\npackage a (\"golang.org/lsptests/godef/a\")\n```\n\n[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a)" +} + +-- AImport-hoverdef -- +```go +package a ("golang.org/lsptests/godef/a") +``` + +[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a) +-- AString-definition -- +godef/a/a.go:26:6-7: defined here as ```go +type A string + +func (a.A).Hi() +``` + +@mark(AString, "A") + + +[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A) +-- AString-definition-json -- +{ + "span": { + "uri": "file://godef/a/a.go", + "start": { + "line": 26, + "column": 6, + "offset": 467 + }, + "end": { + "line": 26, + "column": 7, + "offset": 468 + } + }, + "description": "```go\ntype A string\n\nfunc (a.A).Hi()\n```\n\n@mark(AString, \"A\")\n\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A)" +} + +-- AString-hoverdef -- +```go +type A string + +func (a.A).Hi() +``` + +@mark(AString, "A") + + +[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#A) +-- AStuff-definition -- +godef/a/a.go:28:6-12: defined here as ```go +func a.AStuff() +``` + +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) +-- AStuff-definition-json -- +{ + "span": { + "uri": "file://godef/a/a.go", + "start": { + "line": 28, + "column": 6, + "offset": 504 + }, + "end": { + "line": 28, + "column": 12, + "offset": 510 + } + }, + "description": "```go\nfunc a.AStuff()\n```\n\n[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff)" +} + +-- AStuff-hoverdef -- +```go +func a.AStuff() +``` + +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) +-- S1-definition -- +godef/b/b.go:27:6-8: defined here as ```go +type S1 struct { + F1 int //@mark(S1F1, "F1") + S2 //@godef("S2", S2),mark(S1S2, "S2") + a.A //@godef("A", AString) + aAlias //@godef("a", aAlias) +} +``` + +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) +-- S1-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 27, + "column": 6, + "offset": 563 + }, + "end": { + "line": 27, + "column": 8, + "offset": 565 + } + }, + "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1)" +} + +-- S1-hoverdef -- +```go +type S1 struct { + F1 int //@mark(S1F1, "F1") + S2 //@godef("S2", S2),mark(S1S2, "S2") + a.A //@godef("A", AString) + aAlias //@godef("a", aAlias) +} +``` + +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) +-- S1F1-definition -- +godef/b/b.go:28:2-4: defined here as ```go +field F1 int +``` + +@mark(S1F1, "F1") + + +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) +-- S1F1-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 28, + "column": 2, + "offset": 582 + }, + "end": { + "line": 28, + "column": 4, + "offset": 584 + } + }, + "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1)" +} + +-- S1F1-hoverdef -- +```go +field F1 int +``` + +@mark(S1F1, "F1") + + +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) +-- S1S2-definition -- +godef/b/b.go:29:2-4: defined here as ```go +field S2 S2 +``` + +@godef("S2", S2),mark(S1S2, "S2") + + +[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2) +-- S1S2-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 29, + "column": 2, + "offset": 614 + }, + "end": { + "line": 29, + "column": 4, + "offset": 616 + } + }, + "description": "```go\nfield S2 S2\n```\n\n@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2)" +} + +-- S1S2-hoverdef -- +```go +field S2 S2 +``` + +@godef("S2", S2),mark(S1S2, "S2") + + +[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.S2) +-- S2-definition -- +godef/b/b.go:34:6-8: defined here as ```go +type S2 struct { + F1 string //@mark(S2F1, "F1") + F2 int //@mark(S2F2, "F2") + *a.A //@godef("A", AString),godef("a",AImport) +} +``` + +[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2) +-- S2-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 34, + "column": 6, + "offset": 738 + }, + "end": { + "line": 34, + "column": 8, + "offset": 740 + } + }, + "description": "```go\ntype S2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", AString),godef(\"a\",AImport)\n}\n```\n\n[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2)" +} + +-- S2-hoverdef -- +```go +type S2 struct { + F1 string //@mark(S2F1, "F1") + F2 int //@mark(S2F2, "F2") + *a.A //@godef("A", AString),godef("a",AImport) +} +``` + +[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2) +-- S2F1-definition -- +godef/b/b.go:35:2-4: defined here as ```go +field F1 string +``` + +@mark(S2F1, "F1") + + +[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1) +-- S2F1-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 35, + "column": 2, + "offset": 757 + }, + "end": { + "line": 35, + "column": 4, + "offset": 759 + } + }, + "description": "```go\nfield F1 string\n```\n\n@mark(S2F1, \"F1\")\n\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1)" +} + +-- S2F1-hoverdef -- +```go +field F1 string +``` + +@mark(S2F1, "F1") + + +[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F1) +-- S2F2-definition -- +godef/b/b.go:36:2-4: defined here as ```go +field F2 int +``` + +@mark(S2F2, "F2") + + +[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2) +-- S2F2-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 36, + "column": 2, + "offset": 790 + }, + "end": { + "line": 36, + "column": 4, + "offset": 792 + } + }, + "description": "```go\nfield F2 int\n```\n\n@mark(S2F2, \"F2\")\n\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2)" +} + +-- S2F2-hoverdef -- +```go +field F2 int +``` + +@mark(S2F2, "F2") + + +[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S2.F2) +-- aAlias-definition -- +godef/b/b.go:25:6-12: defined here as ```go +type aAlias = a.A + +func (a.A).Hi() +``` + +@mark(aAlias, "aAlias") +-- aAlias-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 25, + "column": 6, + "offset": 518 + }, + "end": { + "line": 25, + "column": 12, + "offset": 524 + } + }, + "description": "```go\ntype aAlias = a.A\n\nfunc (a.A).Hi()\n```\n\n@mark(aAlias, \"aAlias\")" +} + +-- aAlias-hoverdef -- +```go +type aAlias = a.A + +func (a.A).Hi() +``` + +@mark(aAlias, "aAlias") + +-- bX-definition -- +godef/b/b.go:57:7-8: defined here as ```go +const X untyped int = 0 +``` + +@mark(bX, "X"),godef("X", bX) + + +[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X) +-- bX-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 57, + "column": 7, + "offset": 1225 + }, + "end": { + "line": 57, + "column": 8, + "offset": 1226 + } + }, + "description": "```go\nconst X untyped int = 0\n```\n\n@mark(bX, \"X\"),godef(\"X\", bX)\n\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X)" +} + +-- bX-hoverdef -- +```go +const X untyped int = 0 +``` + +@mark(bX, "X"),godef("X", bX) + + +[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#X) +-- myFoo-definition -- +godef/b/b.go:4:2-7: defined here as ```go +package myFoo ("golang.org/lsptests/foo") +``` + +[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo) +-- myFoo-definition-json -- +{ + "span": { + "uri": "file://godef/b/b.go", + "start": { + "line": 4, + "column": 2, + "offset": 21 + }, + "end": { + "line": 4, + "column": 7, + "offset": 26 + } + }, + "description": "```go\npackage myFoo (\"golang.org/lsptests/foo\")\n```\n\n[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo)" +} + +-- myFoo-hoverdef -- +```go +package myFoo ("golang.org/lsptests/foo") +``` + +[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/foo) diff --git a/internal/lsp/testdata/godef/b/c.go b/gopls/internal/lsp/testdata/godef/b/c.go similarity index 100% rename from internal/lsp/testdata/godef/b/c.go rename to gopls/internal/lsp/testdata/godef/b/c.go diff --git a/internal/lsp/testdata/godef/b/c.go.golden b/gopls/internal/lsp/testdata/godef/b/c.go.golden similarity index 59% rename from internal/lsp/testdata/godef/b/c.go.golden rename to gopls/internal/lsp/testdata/godef/b/c.go.golden index e6205b7265c..575bd1e7b51 100644 --- a/internal/lsp/testdata/godef/b/c.go.golden +++ b/gopls/internal/lsp/testdata/godef/b/c.go.golden @@ -8,7 +8,7 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) -- S1-definition-json -- { "span": { @@ -16,15 +16,15 @@ type S1 struct { "start": { "line": 27, "column": 6, - "offset": 587 + "offset": 563 }, "end": { "line": 27, "column": 8, - "offset": 589 + "offset": 565 } }, - "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1)" + "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1)" } -- S1-hoverdef -- @@ -37,15 +37,16 @@ type S1 struct { } ``` -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1) +[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1) -- S1F1-definition -- godef/b/b.go:28:2-4: defined here as ```go field F1 int ``` -\@mark\(S1F1, \"F1\"\) +@mark(S1F1, "F1") -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1) + +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) -- S1F1-definition-json -- { "span": { @@ -53,15 +54,15 @@ field F1 int "start": { "line": 28, "column": 2, - "offset": 606 + "offset": 582 }, "end": { "line": 28, "column": 4, - "offset": 608 + "offset": 584 } }, - "description": "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1)" + "description": "```go\nfield F1 int\n```\n\n@mark(S1F1, \"F1\")\n\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1)" } -- S1F1-hoverdef -- @@ -69,6 +70,7 @@ field F1 int field F1 int ``` -\@mark\(S1F1, \"F1\"\) +@mark(S1F1, "F1") + -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1) +[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/b#S1.F1) diff --git a/internal/lsp/testdata/godef/b/c.go.saved b/gopls/internal/lsp/testdata/godef/b/c.go.saved similarity index 100% rename from internal/lsp/testdata/godef/b/c.go.saved rename to gopls/internal/lsp/testdata/godef/b/c.go.saved diff --git a/internal/lsp/testdata/godef/b/e.go b/gopls/internal/lsp/testdata/godef/b/e.go similarity index 93% rename from internal/lsp/testdata/godef/b/e.go rename to gopls/internal/lsp/testdata/godef/b/e.go index 7b96cd7e8ae..9c81cad3171 100644 --- a/internal/lsp/testdata/godef/b/e.go +++ b/gopls/internal/lsp/testdata/godef/b/e.go @@ -3,7 +3,7 @@ package b import ( "fmt" - "golang.org/x/tools/internal/lsp/godef/a" + "golang.org/lsptests/godef/a" ) func useThings() { diff --git a/internal/lsp/testdata/godef/b/e.go.golden b/gopls/internal/lsp/testdata/godef/b/e.go.golden similarity index 58% rename from internal/lsp/testdata/godef/b/e.go.golden rename to gopls/internal/lsp/testdata/godef/b/e.go.golden index f9af7b74317..3d7d8979771 100644 --- a/internal/lsp/testdata/godef/b/e.go.golden +++ b/gopls/internal/lsp/testdata/godef/b/e.go.golden @@ -3,9 +3,10 @@ godef/a/d.go:6:2-8: defined here as ```go field Member string ``` -\@Member +@Member -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member) + +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member) -- Member-definition-json -- { "span": { @@ -21,7 +22,7 @@ field Member string "offset": 96 } }, - "description": "```go\nfield Member string\n```\n\n\\@Member\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member)" + "description": "```go\nfield Member string\n```\n\n@Member\n\n\n[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member)" } -- Member-hoverdef -- @@ -29,17 +30,19 @@ field Member string field Member string ``` -\@Member +@Member + -[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing.Member) +[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing.Member) -- Other-definition -- godef/a/d.go:9:5-10: defined here as ```go var a.Other a.Thing ``` -\@Other +@Other + -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other) +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other) -- Other-definition-json -- { "span": { @@ -55,7 +58,7 @@ var a.Other a.Thing "offset": 126 } }, - "description": "```go\nvar a.Other a.Thing\n```\n\n\\@Other\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other)" + "description": "```go\nvar a.Other a.Thing\n```\n\n@Other\n\n\n[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other)" } -- Other-hoverdef -- @@ -63,17 +66,22 @@ var a.Other a.Thing var a.Other a.Thing ``` -\@Other +@Other -[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Other) + +[`a.Other` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Other) -- Thing-definition -- godef/a/d.go:5:6-11: defined here as ```go type Thing struct { Member string //@Member } + +func (a.Thing).Method(i int) string +func (*a.Thing).Method2(i int, j int) (error, string) +func (a.Thing).Method3() ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) -- Thing-definition-json -- { "span": { @@ -89,7 +97,7 @@ type Thing struct { "offset": 70 } }, - "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing)" + "description": "```go\ntype Thing struct {\n\tMember string //@Member\n}\n\nfunc (a.Thing).Method(i int) string\nfunc (*a.Thing).Method2(i int, j int) (error, string)\nfunc (a.Thing).Method3()\n```\n\n[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing)" } -- Thing-hoverdef -- @@ -97,15 +105,19 @@ type Thing struct { type Thing struct { Member string //@Member } + +func (a.Thing).Method(i int) string +func (*a.Thing).Method2(i int, j int) (error, string) +func (a.Thing).Method3() ``` -[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Thing) +[`a.Thing` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Thing) -- Things-definition -- godef/a/d.go:11:6-12: defined here as ```go func a.Things(val []string) []a.Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things) -- Things-definition-json -- { "span": { @@ -121,7 +133,7 @@ func a.Things(val []string) []a.Thing "offset": 154 } }, - "description": "```go\nfunc a.Things(val []string) []a.Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things)" + "description": "```go\nfunc a.Things(val []string) []a.Thing\n```\n\n[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things)" } -- Things-hoverdef -- @@ -129,7 +141,7 @@ func a.Things(val []string) []a.Thing func a.Things(val []string) []a.Thing ``` -[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Things) +[`a.Things` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#Things) -- eInt-hoverdef -- ```go var x int diff --git a/internal/lsp/testdata/godef/b/h.go b/gopls/internal/lsp/testdata/godef/b/h.go similarity index 74% rename from internal/lsp/testdata/godef/b/h.go rename to gopls/internal/lsp/testdata/godef/b/h.go index c8cbe850f9c..88017643336 100644 --- a/internal/lsp/testdata/godef/b/h.go +++ b/gopls/internal/lsp/testdata/godef/b/h.go @@ -1,6 +1,6 @@ package b -import . "golang.org/x/tools/internal/lsp/godef/a" +import . "golang.org/lsptests/godef/a" func _() { // variable of type a.A diff --git a/gopls/internal/lsp/testdata/godef/b/h.go.golden b/gopls/internal/lsp/testdata/godef/b/h.go.golden new file mode 100644 index 00000000000..04c7a291338 --- /dev/null +++ b/gopls/internal/lsp/testdata/godef/b/h.go.golden @@ -0,0 +1,13 @@ +-- AStuff-hoverdef -- +```go +func AStuff() +``` + +[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/a#AStuff) +-- AVariable-hoverdef -- +```go +var _ A +``` + +variable of type a.A + diff --git a/internal/lsp/testdata/godef/broken/unclosedIf.go.golden b/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.golden similarity index 79% rename from internal/lsp/testdata/godef/broken/unclosedIf.go.golden rename to gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.golden index 5c3329d8b67..9ce869848cb 100644 --- a/internal/lsp/testdata/godef/broken/unclosedIf.go.golden +++ b/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.golden @@ -3,7 +3,7 @@ godef/broken/unclosedIf.go:7:7-19: defined here as ```go var myUnclosedIf string ``` -\@myUnclosedIf +@myUnclosedIf -- myUnclosedIf-definition-json -- { "span": { @@ -19,7 +19,7 @@ var myUnclosedIf string "offset": 80 } }, - "description": "```go\nvar myUnclosedIf string\n```\n\n\\@myUnclosedIf" + "description": "```go\nvar myUnclosedIf string\n```\n\n@myUnclosedIf" } -- myUnclosedIf-hoverdef -- @@ -27,4 +27,5 @@ var myUnclosedIf string var myUnclosedIf string ``` -\@myUnclosedIf +@myUnclosedIf + diff --git a/internal/lsp/testdata/godef/broken/unclosedIf.go.in b/gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.in similarity index 100% rename from internal/lsp/testdata/godef/broken/unclosedIf.go.in rename to gopls/internal/lsp/testdata/godef/broken/unclosedIf.go.in diff --git a/internal/lsp/testdata/godef/hover_generics/hover.go b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go similarity index 73% rename from internal/lsp/testdata/godef/hover_generics/hover.go rename to gopls/internal/lsp/testdata/godef/hover_generics/hover.go index 7400e1acdd8..a26980a5e15 100644 --- a/internal/lsp/testdata/godef/hover_generics/hover.go +++ b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go @@ -10,6 +10,8 @@ type Value[T any] struct { //@mark(ValueTdecl, "T"),hoverdef("T",ValueTdecl) Q int //@mark(ValueQfield, "Q"),hoverdef("Q", ValueQfield) } -func F[P interface{ ~int | string }]() { //@mark(Pparam, "P"),hoverdef("P",Pparam) - var _ P //@mark(Pvar, "P"),hoverdef("P",Pvar) +// disabled - see issue #54822 +func F[P interface{ ~int | string }]() { // mark(Pparam, "P"),hoverdef("P",Pparam) + // disabled - see issue #54822 + var _ P // mark(Pvar, "P"),hoverdef("P",Pvar) } diff --git a/internal/lsp/testdata/godef/hover_generics/hover.go.golden b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden similarity index 63% rename from internal/lsp/testdata/godef/hover_generics/hover.go.golden rename to gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden index cfebcc472c9..fb03865bf8b 100644 --- a/internal/lsp/testdata/godef/hover_generics/hover.go.golden +++ b/gopls/internal/lsp/testdata/godef/hover_generics/hover.go.golden @@ -1,19 +1,12 @@ --- Pparam-hoverdef -- -```go -type parameter P interface{~int|string} -``` --- Pvar-hoverdef -- -```go -type parameter P interface{~int|string} -``` -- ValueQfield-hoverdef -- ```go field Q int ``` -\@mark\(ValueQfield, \"Q\"\),hoverdef\(\"Q\", ValueQfield\) +@mark(ValueQfield, "Q"),hoverdef("Q", ValueQfield) + -[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/hover_generics?utm_source=gopls#Value.Q) +[`(hover.Value).Q` on pkg.go.dev](https://pkg.go.dev/golang.org/lsptests/godef/hover_generics#Value.Q) -- ValueTdecl-hoverdef -- ```go type parameter T any @@ -34,7 +27,8 @@ type value[T any] struct { field Q int ``` -\@mark\(valueQfield, \"Q\"\),hoverdef\(\"Q\", valueQfield\) +@mark(valueQfield, "Q"),hoverdef("Q", valueQfield) + -- valueTdecl-hoverdef -- ```go type parameter T any diff --git a/internal/lsp/testdata/godef/infer_generics/inferred.go b/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go similarity index 100% rename from internal/lsp/testdata/godef/infer_generics/inferred.go rename to gopls/internal/lsp/testdata/godef/infer_generics/inferred.go diff --git a/internal/lsp/testdata/godef/infer_generics/inferred.go.golden b/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden similarity index 77% rename from internal/lsp/testdata/godef/infer_generics/inferred.go.golden rename to gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden index 4a36ff460b6..3fcc5f43539 100644 --- a/internal/lsp/testdata/godef/infer_generics/inferred.go.golden +++ b/gopls/internal/lsp/testdata/godef/infer_generics/inferred.go.golden @@ -2,10 +2,6 @@ ```go func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S ``` --- constrInf-hoverdef -- -```go -func app(s []int, e int) []int // func[S₁ interface{~[]E₂}, E₂ interface{}](s S₁, e E₂) S₁ -``` -- constrInfer-hoverdef -- ```go func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S diff --git a/internal/lsp/testdata/good/good0.go b/gopls/internal/lsp/testdata/good/good0.go similarity index 100% rename from internal/lsp/testdata/good/good0.go rename to gopls/internal/lsp/testdata/good/good0.go diff --git a/internal/lsp/testdata/good/good1.go b/gopls/internal/lsp/testdata/good/good1.go similarity index 81% rename from internal/lsp/testdata/good/good1.go rename to gopls/internal/lsp/testdata/good/good1.go index c4664a7e5d4..624d8147af2 100644 --- a/internal/lsp/testdata/good/good1.go +++ b/gopls/internal/lsp/testdata/good/good1.go @@ -1,7 +1,7 @@ package good //@diag("package", "no_diagnostics", "", "error") import ( - "golang.org/x/tools/internal/lsp/types" //@item(types_import, "types", "\"golang.org/x/tools/internal/lsp/types\"", "package") + "golang.org/lsptests/types" //@item(types_import, "types", "\"golang.org/lsptests/types\"", "package") ) func random() int { //@item(good_random, "random", "func() int", "func") @@ -14,6 +14,7 @@ func random2(y int) int { //@item(good_random2, "random2", "func(y int) int", "f //@complete("", good_y_param, types_import, good_random, good_random2, good_stuff) var b types.Bob = &types.X{} //@prepare("ypes","types", "types") if _, ok := b.(*types.X); ok { //@complete("X", X_struct, Y_struct, Bob_interface, CoolAlias) + _ = 0 // suppress "empty branch" diagnostic } return y diff --git a/internal/lsp/testdata/highlights/highlights.go b/gopls/internal/lsp/testdata/highlights/highlights.go similarity index 100% rename from internal/lsp/testdata/highlights/highlights.go rename to gopls/internal/lsp/testdata/highlights/highlights.go diff --git a/internal/lsp/testdata/implementation/implementation.go b/gopls/internal/lsp/testdata/implementation/implementation.go similarity index 93% rename from internal/lsp/testdata/implementation/implementation.go rename to gopls/internal/lsp/testdata/implementation/implementation.go index c3229121a3d..b817319d5ef 100644 --- a/internal/lsp/testdata/implementation/implementation.go +++ b/gopls/internal/lsp/testdata/implementation/implementation.go @@ -1,6 +1,6 @@ package implementation -import "golang.org/x/tools/internal/lsp/implementation/other" +import "golang.org/lsptests/implementation/other" type ImpP struct{} //@ImpP,implementations("ImpP", Laugher, OtherLaugher) diff --git a/internal/lsp/testdata/implementation/other/other.go b/gopls/internal/lsp/testdata/implementation/other/other.go similarity index 100% rename from internal/lsp/testdata/implementation/other/other.go rename to gopls/internal/lsp/testdata/implementation/other/other.go diff --git a/internal/lsp/testdata/implementation/other/other_test.go b/gopls/internal/lsp/testdata/implementation/other/other_test.go similarity index 100% rename from internal/lsp/testdata/implementation/other/other_test.go rename to gopls/internal/lsp/testdata/implementation/other/other_test.go diff --git a/internal/lsp/testdata/importedcomplit/imported_complit.go.in b/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in similarity index 62% rename from internal/lsp/testdata/importedcomplit/imported_complit.go.in rename to gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in index 80d85245cb4..2f4cbada141 100644 --- a/internal/lsp/testdata/importedcomplit/imported_complit.go.in +++ b/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in @@ -1,16 +1,16 @@ package importedcomplit import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" // import completions "fm" //@complete("\" //", fmtImport) "go/pars" //@complete("\" //", parserImport) - "golang.org/x/tools/internal/lsp/signa" //@complete("na\" //", signatureImport) - "golang.org/x/too" //@complete("\" //", toolsImport) + "golang.org/lsptests/signa" //@complete("na\" //", signatureImport) + "golang.org/lspte" //@complete("\" //", lsptestsImport) "crypto/elli" //@complete("\" //", cryptoImport) - "golang.org/x/tools/internal/lsp/sign" //@complete("\" //", signatureImport) - "golang.org/x/tools/internal/lsp/sign" //@complete("ols", toolsImport) + "golang.org/lsptests/sign" //@complete("\" //", signatureImport) + "golang.org/lsptests/sign" //@complete("ests", lsptestsImport) namedParser "go/pars" //@complete("\" //", parserImport) ) @@ -37,6 +37,6 @@ func _() { /* "fmt" */ //@item(fmtImport, "fmt", "\"fmt\"", "package") /* "go/parser" */ //@item(parserImport, "parser", "\"go/parser\"", "package") -/* "golang.org/x/tools/internal/lsp/signature" */ //@item(signatureImport, "signature", "\"golang.org/x/tools/internal/lsp/signature\"", "package") -/* "golang.org/x/tools/" */ //@item(toolsImport, "tools/", "\"golang.org/x/tools/\"", "package") +/* "golang.org/lsptests/signature" */ //@item(signatureImport, "signature", "\"golang.org/lsptests/signature\"", "package") +/* "golang.org/lsptests/" */ //@item(lsptestsImport, "lsptests/", "\"golang.org/lsptests/\"", "package") /* "crypto/elliptic" */ //@item(cryptoImport, "elliptic", "\"crypto/elliptic\"", "package") diff --git a/internal/lsp/testdata/imports/add_import.go.golden b/gopls/internal/lsp/testdata/imports/add_import.go.golden similarity index 100% rename from internal/lsp/testdata/imports/add_import.go.golden rename to gopls/internal/lsp/testdata/imports/add_import.go.golden diff --git a/internal/lsp/testdata/imports/add_import.go.in b/gopls/internal/lsp/testdata/imports/add_import.go.in similarity index 100% rename from internal/lsp/testdata/imports/add_import.go.in rename to gopls/internal/lsp/testdata/imports/add_import.go.in diff --git a/internal/lsp/testdata/imports/good_imports.go.golden b/gopls/internal/lsp/testdata/imports/good_imports.go.golden similarity index 100% rename from internal/lsp/testdata/imports/good_imports.go.golden rename to gopls/internal/lsp/testdata/imports/good_imports.go.golden diff --git a/internal/lsp/testdata/imports/good_imports.go.in b/gopls/internal/lsp/testdata/imports/good_imports.go.in similarity index 100% rename from internal/lsp/testdata/imports/good_imports.go.in rename to gopls/internal/lsp/testdata/imports/good_imports.go.in diff --git a/internal/lsp/testdata/imports/issue35458.go.golden b/gopls/internal/lsp/testdata/imports/issue35458.go.golden similarity index 100% rename from internal/lsp/testdata/imports/issue35458.go.golden rename to gopls/internal/lsp/testdata/imports/issue35458.go.golden diff --git a/internal/lsp/testdata/imports/issue35458.go.in b/gopls/internal/lsp/testdata/imports/issue35458.go.in similarity index 100% rename from internal/lsp/testdata/imports/issue35458.go.in rename to gopls/internal/lsp/testdata/imports/issue35458.go.in diff --git a/internal/lsp/testdata/imports/multiple_blocks.go.golden b/gopls/internal/lsp/testdata/imports/multiple_blocks.go.golden similarity index 100% rename from internal/lsp/testdata/imports/multiple_blocks.go.golden rename to gopls/internal/lsp/testdata/imports/multiple_blocks.go.golden diff --git a/internal/lsp/testdata/imports/multiple_blocks.go.in b/gopls/internal/lsp/testdata/imports/multiple_blocks.go.in similarity index 100% rename from internal/lsp/testdata/imports/multiple_blocks.go.in rename to gopls/internal/lsp/testdata/imports/multiple_blocks.go.in diff --git a/internal/lsp/testdata/imports/needs_imports.go.golden b/gopls/internal/lsp/testdata/imports/needs_imports.go.golden similarity index 100% rename from internal/lsp/testdata/imports/needs_imports.go.golden rename to gopls/internal/lsp/testdata/imports/needs_imports.go.golden diff --git a/internal/lsp/testdata/imports/needs_imports.go.in b/gopls/internal/lsp/testdata/imports/needs_imports.go.in similarity index 100% rename from internal/lsp/testdata/imports/needs_imports.go.in rename to gopls/internal/lsp/testdata/imports/needs_imports.go.in diff --git a/internal/lsp/testdata/imports/remove_import.go.golden b/gopls/internal/lsp/testdata/imports/remove_import.go.golden similarity index 100% rename from internal/lsp/testdata/imports/remove_import.go.golden rename to gopls/internal/lsp/testdata/imports/remove_import.go.golden diff --git a/internal/lsp/testdata/imports/remove_import.go.in b/gopls/internal/lsp/testdata/imports/remove_import.go.in similarity index 100% rename from internal/lsp/testdata/imports/remove_import.go.in rename to gopls/internal/lsp/testdata/imports/remove_import.go.in diff --git a/internal/lsp/testdata/imports/remove_imports.go.golden b/gopls/internal/lsp/testdata/imports/remove_imports.go.golden similarity index 100% rename from internal/lsp/testdata/imports/remove_imports.go.golden rename to gopls/internal/lsp/testdata/imports/remove_imports.go.golden diff --git a/internal/lsp/testdata/imports/remove_imports.go.in b/gopls/internal/lsp/testdata/imports/remove_imports.go.in similarity index 100% rename from internal/lsp/testdata/imports/remove_imports.go.in rename to gopls/internal/lsp/testdata/imports/remove_imports.go.in diff --git a/internal/lsp/testdata/imports/two_lines.go.golden b/gopls/internal/lsp/testdata/imports/two_lines.go.golden similarity index 100% rename from internal/lsp/testdata/imports/two_lines.go.golden rename to gopls/internal/lsp/testdata/imports/two_lines.go.golden diff --git a/internal/lsp/testdata/imports/two_lines.go.in b/gopls/internal/lsp/testdata/imports/two_lines.go.in similarity index 100% rename from internal/lsp/testdata/imports/two_lines.go.in rename to gopls/internal/lsp/testdata/imports/two_lines.go.in diff --git a/internal/lsp/testdata/index/index.go b/gopls/internal/lsp/testdata/index/index.go similarity index 100% rename from internal/lsp/testdata/index/index.go rename to gopls/internal/lsp/testdata/index/index.go diff --git a/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go new file mode 100644 index 00000000000..b05c95ec800 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go @@ -0,0 +1,27 @@ +package inlayHint //@inlayHint("package") + +import "fmt" + +func fieldNames() { + for _, c := range []struct { + in, want string + }{ + struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, + {"Hello, 世界", "界世 ,olleH"}, + {"", ""}, + } { + fmt.Println(c.in == c.want) + } +} + +func fieldNamesPointers() { + for _, c := range []*struct { + in, want string + }{ + &struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, + {"Hello, 世界", "界世 ,olleH"}, + {"", ""}, + } { + fmt.Println(c.in == c.want) + } +} diff --git a/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden new file mode 100644 index 00000000000..eb2febdb6a3 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden @@ -0,0 +1,29 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +import "fmt" + +func fieldNames() { + for _< int>, c< struct{in string; want string}> := range []struct { + in, want string + }{ + struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, + {"Hello, 世界", "界世 ,olleH"}, + {"", ""}, + } { + fmt.Println(c.in == c.want) + } +} + +func fieldNamesPointers() { + for _< int>, c< *struct{in string; want string}> := range []*struct { + in, want string + }{ + &struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, + <&struct{in string; want string}>{"Hello, 世界", "界世 ,olleH"}, + <&struct{in string; want string}>{"", ""}, + } { + fmt.Println(c.in == c.want) + } +} + diff --git a/gopls/internal/lsp/testdata/inlay_hint/constant_values.go b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go new file mode 100644 index 00000000000..e3339b0f303 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go @@ -0,0 +1,45 @@ +package inlayHint //@inlayHint("package") + +const True = true + +type Kind int + +const ( + KindNone Kind = iota + KindPrint + KindPrintf + KindErrorf +) + +const ( + u = iota * 4 + v float64 = iota * 42 + w = iota * 42 +) + +const ( + a, b = 1, 2 + c, d + e, f = 5 * 5, "hello" + "world" + g, h + i, j = true, f +) + +// No hint +const ( + Int = 3 + Float = 3.14 + Bool = true + Rune = '3' + Complex = 2.7i + String = "Hello, world!" +) + +var ( + varInt = 3 + varFloat = 3.14 + varBool = true + varRune = '3' + '4' + varComplex = 2.7i + varString = "Hello, world!" +) diff --git a/gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden new file mode 100644 index 00000000000..edc46debc37 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden @@ -0,0 +1,47 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +const True = true + +type Kind int + +const ( + KindNone Kind = iota< = 0> + KindPrint< = 1> + KindPrintf< = 2> + KindErrorf< = 3> +) + +const ( + u = iota * 4< = 0> + v float64 = iota * 42< = 42> + w = iota * 42< = 84> +) + +const ( + a, b = 1, 2 + c, d< = 1, 2> + e, f = 5 * 5, "hello" + "world"< = 25, "helloworld"> + g, h< = 25, "helloworld"> + i, j = true, f< = true, "helloworld"> +) + +// No hint +const ( + Int = 3 + Float = 3.14 + Bool = true + Rune = '3' + Complex = 2.7i + String = "Hello, world!" +) + +var ( + varInt = 3 + varFloat = 3.14 + varBool = true + varRune = '3' + '4' + varComplex = 2.7i + varString = "Hello, world!" +) + diff --git a/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go new file mode 100644 index 00000000000..0d930e5d426 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go @@ -0,0 +1,50 @@ +package inlayHint //@inlayHint("package") + +import "fmt" + +func hello(name string) string { + return "Hello " + name +} + +func helloWorld() string { + return hello("World") +} + +type foo struct{} + +func (*foo) bar(baz string, qux int) int { + if baz != "" { + return qux + 1 + } + return qux +} + +func kase(foo int, bar bool, baz ...string) { + fmt.Println(foo, bar, baz) +} + +func kipp(foo string, bar, baz string) { + fmt.Println(foo, bar, baz) +} + +func plex(foo, bar string, baz string) { + fmt.Println(foo, bar, baz) +} + +func tars(foo string, bar, baz string) { + fmt.Println(foo, bar, baz) +} + +func foobar() { + var x foo + x.bar("", 1) + kase(0, true, "c", "d", "e") + kipp("a", "b", "c") + plex("a", "b", "c") + tars("a", "b", "c") + foo, bar, baz := "a", "b", "c" + kipp(foo, bar, baz) + plex("a", bar, baz) + tars(foo+foo, (bar), "c") + +} diff --git a/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden new file mode 100644 index 00000000000..4e93a4f9268 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden @@ -0,0 +1,52 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +import "fmt" + +func hello(name string) string { + return "Hello " + name +} + +func helloWorld() string { + return hello("World") +} + +type foo struct{} + +func (*foo) bar(baz string, qux int) int { + if baz != "" { + return qux + 1 + } + return qux +} + +func kase(foo int, bar bool, baz ...string) { + fmt.Println(foo, bar, baz) +} + +func kipp(foo string, bar, baz string) { + fmt.Println(foo, bar, baz) +} + +func plex(foo, bar string, baz string) { + fmt.Println(foo, bar, baz) +} + +func tars(foo string, bar, baz string) { + fmt.Println(foo, bar, baz) +} + +func foobar() { + var x foo + x.bar("", 1) + kase(0, true, "c", "d", "e") + kipp("a", "b", "c") + plex("a", "b", "c") + tars("a", "b", "c") + foo< string>, bar< string>, baz< string> := "a", "b", "c" + kipp(foo, bar, baz) + plex("a", bar, baz) + tars(foo+foo, (bar), "c") + +} + diff --git a/gopls/internal/lsp/testdata/inlay_hint/type_params.go b/gopls/internal/lsp/testdata/inlay_hint/type_params.go new file mode 100644 index 00000000000..3a3c7e53734 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/type_params.go @@ -0,0 +1,45 @@ +//go:build go1.18 +// +build go1.18 + +package inlayHint //@inlayHint("package") + +func main() { + ints := map[string]int64{ + "first": 34, + "second": 12, + } + + floats := map[string]float64{ + "first": 35.98, + "second": 26.99, + } + + SumIntsOrFloats[string, int64](ints) + SumIntsOrFloats[string, float64](floats) + + SumIntsOrFloats(ints) + SumIntsOrFloats(floats) + + SumNumbers(ints) + SumNumbers(floats) +} + +type Number interface { + int64 | float64 +} + +func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { + var s V + for _, v := range m { + s += v + } + return s +} + +func SumNumbers[K comparable, V Number](m map[K]V) V { + var s V + for _, v := range m { + s += v + } + return s +} diff --git a/gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden b/gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden new file mode 100644 index 00000000000..4819963b7a4 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden @@ -0,0 +1,47 @@ +-- inlayHint -- +//go:build go1.18 +// +build go1.18 + +package inlayHint //@inlayHint("package") + +func main() { + ints< map[string]int64> := map[string]int64{ + "first": 34, + "second": 12, + } + + floats< map[string]float64> := map[string]float64{ + "first": 35.98, + "second": 26.99, + } + + SumIntsOrFloats[string, int64](ints) + SumIntsOrFloats[string, float64](floats) + + SumIntsOrFloats<[string, int64]>(ints) + SumIntsOrFloats<[string, float64]>(floats) + + SumNumbers<[string, int64]>(ints) + SumNumbers<[string, float64]>(floats) +} + +type Number interface { + int64 | float64 +} + +func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { + var s V + for _< K>, v< V> := range m { + s += v + } + return s +} + +func SumNumbers[K comparable, V Number](m map[K]V) V { + var s V + for _< K>, v< V> := range m { + s += v + } + return s +} + diff --git a/gopls/internal/lsp/testdata/inlay_hint/variable_types.go b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go new file mode 100644 index 00000000000..219af7059c7 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go @@ -0,0 +1,20 @@ +package inlayHint //@inlayHint("package") + +func assignTypes() { + i, j := 0, len([]string{})-1 + println(i, j) +} + +func rangeTypes() { + for k, v := range []string{} { + println(k, v) + } +} + +func funcLitType() { + myFunc := func(a string) string { return "" } +} + +func compositeLitType() { + foo := map[string]interface{}{"": ""} +} diff --git a/gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden new file mode 100644 index 00000000000..6039950d5f3 --- /dev/null +++ b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden @@ -0,0 +1,22 @@ +-- inlayHint -- +package inlayHint //@inlayHint("package") + +func assignTypes() { + i< int>, j< int> := 0, len([]string{})-1 + println(i, j) +} + +func rangeTypes() { + for k< int>, v< string> := range []string{} { + println(k, v) + } +} + +func funcLitType() { + myFunc< func(a string) string> := func(a string) string { return "" } +} + +func compositeLitType() { + foo< map[string]interface{}> := map[string]interface{}{"": ""} +} + diff --git a/internal/lsp/testdata/interfacerank/interface_rank.go b/gopls/internal/lsp/testdata/interfacerank/interface_rank.go similarity index 100% rename from internal/lsp/testdata/interfacerank/interface_rank.go rename to gopls/internal/lsp/testdata/interfacerank/interface_rank.go diff --git a/gopls/internal/lsp/testdata/issues/issue56505.go b/gopls/internal/lsp/testdata/issues/issue56505.go new file mode 100644 index 00000000000..8c641bfb852 --- /dev/null +++ b/gopls/internal/lsp/testdata/issues/issue56505.go @@ -0,0 +1,8 @@ +package issues + +// Test for golang/go#56505: completion on variables of type *error should not +// panic. +func _() { + var e *error + e.x //@complete(" //") +} diff --git a/internal/lsp/testdata/keywords/accidental_keywords.go.in b/gopls/internal/lsp/testdata/keywords/accidental_keywords.go.in similarity index 100% rename from internal/lsp/testdata/keywords/accidental_keywords.go.in rename to gopls/internal/lsp/testdata/keywords/accidental_keywords.go.in diff --git a/internal/lsp/testdata/keywords/empty_select.go b/gopls/internal/lsp/testdata/keywords/empty_select.go similarity index 100% rename from internal/lsp/testdata/keywords/empty_select.go rename to gopls/internal/lsp/testdata/keywords/empty_select.go diff --git a/internal/lsp/testdata/keywords/empty_switch.go b/gopls/internal/lsp/testdata/keywords/empty_switch.go similarity index 100% rename from internal/lsp/testdata/keywords/empty_switch.go rename to gopls/internal/lsp/testdata/keywords/empty_switch.go diff --git a/internal/lsp/testdata/keywords/keywords.go b/gopls/internal/lsp/testdata/keywords/keywords.go similarity index 100% rename from internal/lsp/testdata/keywords/keywords.go rename to gopls/internal/lsp/testdata/keywords/keywords.go diff --git a/internal/lsp/testdata/labels/labels.go b/gopls/internal/lsp/testdata/labels/labels.go similarity index 100% rename from internal/lsp/testdata/labels/labels.go rename to gopls/internal/lsp/testdata/labels/labels.go diff --git a/internal/lsp/testdata/links/links.go b/gopls/internal/lsp/testdata/links/links.go similarity index 73% rename from internal/lsp/testdata/links/links.go rename to gopls/internal/lsp/testdata/links/links.go index 89492bafebf..378134341b4 100644 --- a/internal/lsp/testdata/links/links.go +++ b/gopls/internal/lsp/testdata/links/links.go @@ -1,11 +1,11 @@ package links import ( - "fmt" //@link(`fmt`,"https://pkg.go.dev/fmt?utm_source=gopls") + "fmt" //@link(`fmt`,"https://pkg.go.dev/fmt") - "golang.org/x/tools/internal/lsp/foo" //@link(`golang.org/x/tools/internal/lsp/foo`,`https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo?utm_source=gopls`) + "golang.org/lsptests/foo" //@link(`golang.org/lsptests/foo`,`https://pkg.go.dev/golang.org/lsptests/foo`) - _ "database/sql" //@link(`database/sql`, `https://pkg.go.dev/database/sql?utm_source=gopls`) + _ "database/sql" //@link(`database/sql`, `https://pkg.go.dev/database/sql`) ) var ( diff --git a/internal/lsp/testdata/maps/maps.go.in b/gopls/internal/lsp/testdata/maps/maps.go.in similarity index 100% rename from internal/lsp/testdata/maps/maps.go.in rename to gopls/internal/lsp/testdata/maps/maps.go.in diff --git a/internal/lsp/testdata/missingfunction/channels.go b/gopls/internal/lsp/testdata/missingfunction/channels.go similarity index 94% rename from internal/lsp/testdata/missingfunction/channels.go rename to gopls/internal/lsp/testdata/missingfunction/channels.go index 436491c1949..303770cd7aa 100644 --- a/internal/lsp/testdata/missingfunction/channels.go +++ b/gopls/internal/lsp/testdata/missingfunction/channels.go @@ -1,7 +1,7 @@ package missingfunction func channels(s string) { - undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix") + undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix", "") } func c() (<-chan string, chan string) { diff --git a/internal/lsp/testdata/missingfunction/channels.go.golden b/gopls/internal/lsp/testdata/missingfunction/channels.go.golden similarity index 96% rename from internal/lsp/testdata/missingfunction/channels.go.golden rename to gopls/internal/lsp/testdata/missingfunction/channels.go.golden index f5078fed17a..998ce589e1d 100644 --- a/internal/lsp/testdata/missingfunction/channels.go.golden +++ b/gopls/internal/lsp/testdata/missingfunction/channels.go.golden @@ -2,7 +2,7 @@ package missingfunction func channels(s string) { - undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix") + undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix", "") } func undefinedChannels(ch1 <-chan string, ch2 chan string) { diff --git a/internal/lsp/testdata/missingfunction/consecutive_params.go b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go similarity index 80% rename from internal/lsp/testdata/missingfunction/consecutive_params.go rename to gopls/internal/lsp/testdata/missingfunction/consecutive_params.go index d2ec3be3232..f2fb3c04132 100644 --- a/internal/lsp/testdata/missingfunction/consecutive_params.go +++ b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go @@ -2,5 +2,5 @@ package missingfunction func consecutiveParams() { var s string - undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix") + undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/consecutive_params.go.golden b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden similarity index 88% rename from internal/lsp/testdata/missingfunction/consecutive_params.go.golden rename to gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden index 14a766496fb..4b852ce141b 100644 --- a/internal/lsp/testdata/missingfunction/consecutive_params.go.golden +++ b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden @@ -3,7 +3,7 @@ package missingfunction func consecutiveParams() { var s string - undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix") + undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix", "") } func undefinedConsecutiveParams(s1, s2 string) { diff --git a/internal/lsp/testdata/missingfunction/error_param.go b/gopls/internal/lsp/testdata/missingfunction/error_param.go similarity index 88% rename from internal/lsp/testdata/missingfunction/error_param.go rename to gopls/internal/lsp/testdata/missingfunction/error_param.go index 9fd943ffb6d..d0484f0ff56 100644 --- a/internal/lsp/testdata/missingfunction/error_param.go +++ b/gopls/internal/lsp/testdata/missingfunction/error_param.go @@ -2,5 +2,5 @@ package missingfunction func errorParam() { var err error - undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix") + undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/error_param.go.golden b/gopls/internal/lsp/testdata/missingfunction/error_param.go.golden similarity index 93% rename from internal/lsp/testdata/missingfunction/error_param.go.golden rename to gopls/internal/lsp/testdata/missingfunction/error_param.go.golden index 2e12711817d..de78646a5f1 100644 --- a/internal/lsp/testdata/missingfunction/error_param.go.golden +++ b/gopls/internal/lsp/testdata/missingfunction/error_param.go.golden @@ -3,7 +3,7 @@ package missingfunction func errorParam() { var err error - undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix") + undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix", "") } func undefinedErrorParam(err error) { diff --git a/internal/lsp/testdata/missingfunction/literals.go b/gopls/internal/lsp/testdata/missingfunction/literals.go similarity index 78% rename from internal/lsp/testdata/missingfunction/literals.go rename to gopls/internal/lsp/testdata/missingfunction/literals.go index e276eae79ec..0099b1a08ad 100644 --- a/internal/lsp/testdata/missingfunction/literals.go +++ b/gopls/internal/lsp/testdata/missingfunction/literals.go @@ -3,5 +3,5 @@ package missingfunction type T struct{} func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix") + undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix", "") } diff --git a/gopls/internal/lsp/testdata/missingfunction/literals.go.golden b/gopls/internal/lsp/testdata/missingfunction/literals.go.golden new file mode 100644 index 00000000000..cb85de4eb11 --- /dev/null +++ b/gopls/internal/lsp/testdata/missingfunction/literals.go.golden @@ -0,0 +1,13 @@ +-- suggestedfix_literals_6_2 -- +package missingfunction + +type T struct{} + +func literals() { + undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix", "") +} + +func undefinedLiterals(s string, t1 T, t2 *T) { + panic("unimplemented") +} + diff --git a/internal/lsp/testdata/missingfunction/operation.go b/gopls/internal/lsp/testdata/missingfunction/operation.go similarity index 81% rename from internal/lsp/testdata/missingfunction/operation.go rename to gopls/internal/lsp/testdata/missingfunction/operation.go index 0408219fe37..a4913ec10b2 100644 --- a/internal/lsp/testdata/missingfunction/operation.go +++ b/gopls/internal/lsp/testdata/missingfunction/operation.go @@ -3,5 +3,5 @@ package missingfunction import "time" func operation() { - undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix") + undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix", "") } diff --git a/gopls/internal/lsp/testdata/missingfunction/operation.go.golden b/gopls/internal/lsp/testdata/missingfunction/operation.go.golden new file mode 100644 index 00000000000..6f9e6ffab6d --- /dev/null +++ b/gopls/internal/lsp/testdata/missingfunction/operation.go.golden @@ -0,0 +1,13 @@ +-- suggestedfix_operation_6_2 -- +package missingfunction + +import "time" + +func operation() { + undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix", "") +} + +func undefinedOperation(duration time.Duration) { + panic("unimplemented") +} + diff --git a/internal/lsp/testdata/missingfunction/selector.go b/gopls/internal/lsp/testdata/missingfunction/selector.go similarity index 90% rename from internal/lsp/testdata/missingfunction/selector.go rename to gopls/internal/lsp/testdata/missingfunction/selector.go index afd1ab61f3a..93a04027138 100644 --- a/internal/lsp/testdata/missingfunction/selector.go +++ b/gopls/internal/lsp/testdata/missingfunction/selector.go @@ -2,5 +2,5 @@ package missingfunction func selector() { m := map[int]bool{} - undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix") + undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/selector.go.golden b/gopls/internal/lsp/testdata/missingfunction/selector.go.golden similarity index 94% rename from internal/lsp/testdata/missingfunction/selector.go.golden rename to gopls/internal/lsp/testdata/missingfunction/selector.go.golden index c48691c4ed5..44e2dde3aa7 100644 --- a/internal/lsp/testdata/missingfunction/selector.go.golden +++ b/gopls/internal/lsp/testdata/missingfunction/selector.go.golden @@ -3,7 +3,7 @@ package missingfunction func selector() { m := map[int]bool{} - undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix") + undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix", "") } func undefinedSelector(b bool) { diff --git a/internal/lsp/testdata/missingfunction/slice.go b/gopls/internal/lsp/testdata/missingfunction/slice.go similarity index 87% rename from internal/lsp/testdata/missingfunction/slice.go rename to gopls/internal/lsp/testdata/missingfunction/slice.go index 4a562a2e762..48b1a52b3f3 100644 --- a/internal/lsp/testdata/missingfunction/slice.go +++ b/gopls/internal/lsp/testdata/missingfunction/slice.go @@ -1,5 +1,5 @@ package missingfunction func slice() { - undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix") + undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix", "") } diff --git a/internal/lsp/testdata/missingfunction/slice.go.golden b/gopls/internal/lsp/testdata/missingfunction/slice.go.golden similarity index 92% rename from internal/lsp/testdata/missingfunction/slice.go.golden rename to gopls/internal/lsp/testdata/missingfunction/slice.go.golden index 0ccb8611b6c..2a05d9a0f54 100644 --- a/internal/lsp/testdata/missingfunction/slice.go.golden +++ b/gopls/internal/lsp/testdata/missingfunction/slice.go.golden @@ -2,7 +2,7 @@ package missingfunction func slice() { - undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix") + undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix", "") } func undefinedSlice(i []int) { diff --git a/internal/lsp/testdata/missingfunction/tuple.go b/gopls/internal/lsp/testdata/missingfunction/tuple.go similarity index 95% rename from internal/lsp/testdata/missingfunction/tuple.go rename to gopls/internal/lsp/testdata/missingfunction/tuple.go index 1c4782c15dd..4059ced983a 100644 --- a/internal/lsp/testdata/missingfunction/tuple.go +++ b/gopls/internal/lsp/testdata/missingfunction/tuple.go @@ -1,7 +1,7 @@ package missingfunction func tuple() { - undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix") + undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix", "") } func b() (string, error) { diff --git a/internal/lsp/testdata/missingfunction/tuple.go.golden b/gopls/internal/lsp/testdata/missingfunction/tuple.go.golden similarity index 97% rename from internal/lsp/testdata/missingfunction/tuple.go.golden rename to gopls/internal/lsp/testdata/missingfunction/tuple.go.golden index 1e12bb70860..e1118a3f348 100644 --- a/internal/lsp/testdata/missingfunction/tuple.go.golden +++ b/gopls/internal/lsp/testdata/missingfunction/tuple.go.golden @@ -2,7 +2,7 @@ package missingfunction func tuple() { - undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix") + undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix", "") } func undefinedTuple(s string, err error) { diff --git a/internal/lsp/testdata/missingfunction/unique_params.go b/gopls/internal/lsp/testdata/missingfunction/unique_params.go similarity index 81% rename from internal/lsp/testdata/missingfunction/unique_params.go rename to gopls/internal/lsp/testdata/missingfunction/unique_params.go index ffaba3f9cb9..00479bf7554 100644 --- a/internal/lsp/testdata/missingfunction/unique_params.go +++ b/gopls/internal/lsp/testdata/missingfunction/unique_params.go @@ -3,5 +3,5 @@ package missingfunction func uniqueArguments() { var s string var i int - undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix") + undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix", "") } diff --git a/gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden b/gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden new file mode 100644 index 00000000000..8d6352cded4 --- /dev/null +++ b/gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden @@ -0,0 +1,13 @@ +-- suggestedfix_unique_params_6_2 -- +package missingfunction + +func uniqueArguments() { + var s string + var i int + undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix", "") +} + +func undefinedUniqueArguments(s1 string, i int, s2 string) { + panic("unimplemented") +} + diff --git a/internal/lsp/testdata/multireturn/multi_return.go.in b/gopls/internal/lsp/testdata/multireturn/multi_return.go.in similarity index 100% rename from internal/lsp/testdata/multireturn/multi_return.go.in rename to gopls/internal/lsp/testdata/multireturn/multi_return.go.in diff --git a/internal/lsp/testdata/nested_complit/nested_complit.go.in b/gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in similarity index 75% rename from internal/lsp/testdata/nested_complit/nested_complit.go.in rename to gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in index 1dddd5b1b53..3ad2d213e98 100644 --- a/internal/lsp/testdata/nested_complit/nested_complit.go.in +++ b/gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in @@ -9,6 +9,7 @@ type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct") func _() { []ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var") _ := ncBar{ - baz: [] //@complete(" //", structNCFoo, structNCBar) + // disabled - see issue #54822 + baz: [] // complete(" //", structNCFoo, structNCBar) } } diff --git a/internal/lsp/testdata/nodisk/empty b/gopls/internal/lsp/testdata/nodisk/empty similarity index 100% rename from internal/lsp/testdata/nodisk/empty rename to gopls/internal/lsp/testdata/nodisk/empty diff --git a/internal/lsp/testdata/nodisk/nodisk.overlay.go b/gopls/internal/lsp/testdata/nodisk/nodisk.overlay.go similarity index 70% rename from internal/lsp/testdata/nodisk/nodisk.overlay.go rename to gopls/internal/lsp/testdata/nodisk/nodisk.overlay.go index f9194be569c..08aebd12f7b 100644 --- a/internal/lsp/testdata/nodisk/nodisk.overlay.go +++ b/gopls/internal/lsp/testdata/nodisk/nodisk.overlay.go @@ -1,7 +1,7 @@ package nodisk import ( - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/noparse/noparse.go.in b/gopls/internal/lsp/testdata/noparse/noparse.go.in similarity index 100% rename from internal/lsp/testdata/noparse/noparse.go.in rename to gopls/internal/lsp/testdata/noparse/noparse.go.in diff --git a/internal/lsp/testdata/noparse_format/noparse_format.go.golden b/gopls/internal/lsp/testdata/noparse_format/noparse_format.go.golden similarity index 100% rename from internal/lsp/testdata/noparse_format/noparse_format.go.golden rename to gopls/internal/lsp/testdata/noparse_format/noparse_format.go.golden diff --git a/internal/lsp/testdata/noparse_format/noparse_format.go.in b/gopls/internal/lsp/testdata/noparse_format/noparse_format.go.in similarity index 100% rename from internal/lsp/testdata/noparse_format/noparse_format.go.in rename to gopls/internal/lsp/testdata/noparse_format/noparse_format.go.in diff --git a/internal/lsp/testdata/noparse_format/parse_format.go.golden b/gopls/internal/lsp/testdata/noparse_format/parse_format.go.golden similarity index 100% rename from internal/lsp/testdata/noparse_format/parse_format.go.golden rename to gopls/internal/lsp/testdata/noparse_format/parse_format.go.golden diff --git a/internal/lsp/testdata/noparse_format/parse_format.go.in b/gopls/internal/lsp/testdata/noparse_format/parse_format.go.in similarity index 100% rename from internal/lsp/testdata/noparse_format/parse_format.go.in rename to gopls/internal/lsp/testdata/noparse_format/parse_format.go.in diff --git a/internal/lsp/testdata/printf/printf.go b/gopls/internal/lsp/testdata/printf/printf.go similarity index 100% rename from internal/lsp/testdata/printf/printf.go rename to gopls/internal/lsp/testdata/printf/printf.go diff --git a/internal/lsp/testdata/rank/assign_rank.go.in b/gopls/internal/lsp/testdata/rank/assign_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/assign_rank.go.in rename to gopls/internal/lsp/testdata/rank/assign_rank.go.in diff --git a/internal/lsp/testdata/rank/binexpr_rank.go.in b/gopls/internal/lsp/testdata/rank/binexpr_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/binexpr_rank.go.in rename to gopls/internal/lsp/testdata/rank/binexpr_rank.go.in diff --git a/internal/lsp/testdata/rank/boolexpr_rank.go b/gopls/internal/lsp/testdata/rank/boolexpr_rank.go similarity index 100% rename from internal/lsp/testdata/rank/boolexpr_rank.go rename to gopls/internal/lsp/testdata/rank/boolexpr_rank.go diff --git a/internal/lsp/testdata/rank/convert_rank.go.in b/gopls/internal/lsp/testdata/rank/convert_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/convert_rank.go.in rename to gopls/internal/lsp/testdata/rank/convert_rank.go.in diff --git a/internal/lsp/testdata/rank/struct/struct_rank.go b/gopls/internal/lsp/testdata/rank/struct/struct_rank.go similarity index 100% rename from internal/lsp/testdata/rank/struct/struct_rank.go rename to gopls/internal/lsp/testdata/rank/struct/struct_rank.go diff --git a/internal/lsp/testdata/rank/switch_rank.go.in b/gopls/internal/lsp/testdata/rank/switch_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/switch_rank.go.in rename to gopls/internal/lsp/testdata/rank/switch_rank.go.in diff --git a/internal/lsp/testdata/rank/type_assert_rank.go.in b/gopls/internal/lsp/testdata/rank/type_assert_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/type_assert_rank.go.in rename to gopls/internal/lsp/testdata/rank/type_assert_rank.go.in diff --git a/internal/lsp/testdata/rank/type_switch_rank.go.in b/gopls/internal/lsp/testdata/rank/type_switch_rank.go.in similarity index 100% rename from internal/lsp/testdata/rank/type_switch_rank.go.in rename to gopls/internal/lsp/testdata/rank/type_switch_rank.go.in diff --git a/internal/lsp/testdata/references/another/another.go b/gopls/internal/lsp/testdata/references/another/another.go similarity index 82% rename from internal/lsp/testdata/references/another/another.go rename to gopls/internal/lsp/testdata/references/another/another.go index 47bda1e4acf..20e3ebca1cb 100644 --- a/internal/lsp/testdata/references/another/another.go +++ b/gopls/internal/lsp/testdata/references/another/another.go @@ -2,7 +2,7 @@ package another import ( - other "golang.org/x/tools/internal/lsp/references/other" + other "golang.org/lsptests/references/other" ) func _() { diff --git a/internal/lsp/testdata/references/interfaces/interfaces.go b/gopls/internal/lsp/testdata/references/interfaces/interfaces.go similarity index 100% rename from internal/lsp/testdata/references/interfaces/interfaces.go rename to gopls/internal/lsp/testdata/references/interfaces/interfaces.go diff --git a/internal/lsp/testdata/references/other/other.go b/gopls/internal/lsp/testdata/references/other/other.go similarity index 83% rename from internal/lsp/testdata/references/other/other.go rename to gopls/internal/lsp/testdata/references/other/other.go index de35cc81a9e..daac1a0282b 100644 --- a/internal/lsp/testdata/references/other/other.go +++ b/gopls/internal/lsp/testdata/references/other/other.go @@ -1,7 +1,7 @@ package other import ( - references "golang.org/x/tools/internal/lsp/references" + references "golang.org/lsptests/references" ) func GetXes() []references.X { diff --git a/internal/lsp/testdata/references/refs.go b/gopls/internal/lsp/testdata/references/refs.go similarity index 100% rename from internal/lsp/testdata/references/refs.go rename to gopls/internal/lsp/testdata/references/refs.go diff --git a/internal/lsp/testdata/references/refs_test.go b/gopls/internal/lsp/testdata/references/refs_test.go similarity index 100% rename from internal/lsp/testdata/references/refs_test.go rename to gopls/internal/lsp/testdata/references/refs_test.go diff --git a/internal/lsp/testdata/rename/a/random.go.golden b/gopls/internal/lsp/testdata/rename/a/random.go.golden similarity index 100% rename from internal/lsp/testdata/rename/a/random.go.golden rename to gopls/internal/lsp/testdata/rename/a/random.go.golden diff --git a/internal/lsp/testdata/rename/a/random.go.in b/gopls/internal/lsp/testdata/rename/a/random.go.in similarity index 100% rename from internal/lsp/testdata/rename/a/random.go.in rename to gopls/internal/lsp/testdata/rename/a/random.go.in diff --git a/internal/lsp/testdata/rename/b/b.go b/gopls/internal/lsp/testdata/rename/b/b.go similarity index 100% rename from internal/lsp/testdata/rename/b/b.go rename to gopls/internal/lsp/testdata/rename/b/b.go diff --git a/internal/lsp/testdata/rename/b/b.go.golden b/gopls/internal/lsp/testdata/rename/b/b.go.golden similarity index 95% rename from internal/lsp/testdata/rename/b/b.go.golden rename to gopls/internal/lsp/testdata/rename/b/b.go.golden index 9cdc5677fd4..36c6d39d0e8 100644 --- a/internal/lsp/testdata/rename/b/b.go.golden +++ b/gopls/internal/lsp/testdata/rename/b/b.go.golden @@ -46,7 +46,7 @@ func Goodbye() {} //@rename("Hello", "Goodbye") c.go: package c -import "golang.org/x/tools/internal/lsp/rename/b" +import "golang.org/lsptests/rename/b" func _() { b.Goodbye() //@rename("Hello", "Goodbye") diff --git a/gopls/internal/lsp/testdata/rename/bad/bad.go.golden b/gopls/internal/lsp/testdata/rename/bad/bad.go.golden new file mode 100644 index 00000000000..1b27e1782f3 --- /dev/null +++ b/gopls/internal/lsp/testdata/rename/bad/bad.go.golden @@ -0,0 +1,2 @@ +-- rFunc-rename -- +renaming "sFunc" to "rFunc" not possible because "golang.org/lsptests/rename/bad" has errors diff --git a/internal/lsp/testdata/rename/bad/bad.go.in b/gopls/internal/lsp/testdata/rename/bad/bad.go.in similarity index 100% rename from internal/lsp/testdata/rename/bad/bad.go.in rename to gopls/internal/lsp/testdata/rename/bad/bad.go.in diff --git a/internal/lsp/testdata/rename/bad/bad_test.go.in b/gopls/internal/lsp/testdata/rename/bad/bad_test.go.in similarity index 100% rename from internal/lsp/testdata/rename/bad/bad_test.go.in rename to gopls/internal/lsp/testdata/rename/bad/bad_test.go.in diff --git a/internal/lsp/testdata/rename/c/c.go b/gopls/internal/lsp/testdata/rename/c/c.go similarity index 56% rename from internal/lsp/testdata/rename/c/c.go rename to gopls/internal/lsp/testdata/rename/c/c.go index 519d2f6fcdf..6332c78f3f9 100644 --- a/internal/lsp/testdata/rename/c/c.go +++ b/gopls/internal/lsp/testdata/rename/c/c.go @@ -1,6 +1,6 @@ package c -import "golang.org/x/tools/internal/lsp/rename/b" +import "golang.org/lsptests/rename/b" func _() { b.Hello() //@rename("Hello", "Goodbye") diff --git a/internal/lsp/testdata/rename/c/c.go.golden b/gopls/internal/lsp/testdata/rename/c/c.go.golden similarity index 88% rename from internal/lsp/testdata/rename/c/c.go.golden rename to gopls/internal/lsp/testdata/rename/c/c.go.golden index 56937420c59..d56250693a9 100644 --- a/internal/lsp/testdata/rename/c/c.go.golden +++ b/gopls/internal/lsp/testdata/rename/c/c.go.golden @@ -24,7 +24,7 @@ func Goodbye() {} //@rename("Hello", "Goodbye") c.go: package c -import "golang.org/x/tools/internal/lsp/rename/b" +import "golang.org/lsptests/rename/b" func _() { b.Goodbye() //@rename("Hello", "Goodbye") diff --git a/internal/lsp/testdata/rename/c/c2.go b/gopls/internal/lsp/testdata/rename/c/c2.go similarity index 100% rename from internal/lsp/testdata/rename/c/c2.go rename to gopls/internal/lsp/testdata/rename/c/c2.go diff --git a/internal/lsp/testdata/rename/c/c2.go.golden b/gopls/internal/lsp/testdata/rename/c/c2.go.golden similarity index 100% rename from internal/lsp/testdata/rename/c/c2.go.golden rename to gopls/internal/lsp/testdata/rename/c/c2.go.golden diff --git a/internal/lsp/testdata/rename/crosspkg/another/another.go b/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/another/another.go rename to gopls/internal/lsp/testdata/rename/crosspkg/another/another.go diff --git a/internal/lsp/testdata/rename/crosspkg/another/another.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go.golden similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/another/another.go.golden rename to gopls/internal/lsp/testdata/rename/crosspkg/another/another.go.golden diff --git a/internal/lsp/testdata/rename/crosspkg/crosspkg.go b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go similarity index 100% rename from internal/lsp/testdata/rename/crosspkg/crosspkg.go rename to gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go diff --git a/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden similarity index 81% rename from internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden rename to gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden index 810926de627..49ff7f841cf 100644 --- a/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden +++ b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden @@ -11,7 +11,7 @@ var Bar int //@rename("Bar", "Tomato") other.go: package other -import "golang.org/x/tools/internal/lsp/rename/crosspkg" +import "golang.org/lsptests/rename/crosspkg" func Other() { crosspkg.Bar @@ -31,7 +31,7 @@ var Tomato int //@rename("Bar", "Tomato") other.go: package other -import "golang.org/x/tools/internal/lsp/rename/crosspkg" +import "golang.org/lsptests/rename/crosspkg" func Other() { crosspkg.Tomato diff --git a/internal/lsp/testdata/rename/crosspkg/other/other.go b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go similarity index 61% rename from internal/lsp/testdata/rename/crosspkg/other/other.go rename to gopls/internal/lsp/testdata/rename/crosspkg/other/other.go index 10d17cd34b5..5fd147da62e 100644 --- a/internal/lsp/testdata/rename/crosspkg/other/other.go +++ b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go @@ -1,6 +1,6 @@ package other -import "golang.org/x/tools/internal/lsp/rename/crosspkg" +import "golang.org/lsptests/rename/crosspkg" func Other() { crosspkg.Bar diff --git a/internal/lsp/testdata/rename/crosspkg/other/other.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden similarity index 81% rename from internal/lsp/testdata/rename/crosspkg/other/other.go.golden rename to gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden index 2722ad96e61..f7b4aaad42f 100644 --- a/internal/lsp/testdata/rename/crosspkg/other/other.go.golden +++ b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden @@ -11,7 +11,7 @@ var Bar int //@rename("Bar", "Tomato") other.go: package other -import "golang.org/x/tools/internal/lsp/rename/crosspkg" +import "golang.org/lsptests/rename/crosspkg" func Other() { crosspkg.Bar diff --git a/internal/lsp/testdata/rename/generics/embedded.go b/gopls/internal/lsp/testdata/rename/generics/embedded.go similarity index 100% rename from internal/lsp/testdata/rename/generics/embedded.go rename to gopls/internal/lsp/testdata/rename/generics/embedded.go diff --git a/internal/lsp/testdata/rename/generics/embedded.go.golden b/gopls/internal/lsp/testdata/rename/generics/embedded.go.golden similarity index 100% rename from internal/lsp/testdata/rename/generics/embedded.go.golden rename to gopls/internal/lsp/testdata/rename/generics/embedded.go.golden diff --git a/internal/lsp/testdata/rename/generics/generics.go b/gopls/internal/lsp/testdata/rename/generics/generics.go similarity index 100% rename from internal/lsp/testdata/rename/generics/generics.go rename to gopls/internal/lsp/testdata/rename/generics/generics.go diff --git a/internal/lsp/testdata/rename/generics/generics.go.golden b/gopls/internal/lsp/testdata/rename/generics/generics.go.golden similarity index 100% rename from internal/lsp/testdata/rename/generics/generics.go.golden rename to gopls/internal/lsp/testdata/rename/generics/generics.go.golden diff --git a/internal/lsp/testdata/rename/generics/unions.go b/gopls/internal/lsp/testdata/rename/generics/unions.go similarity index 100% rename from internal/lsp/testdata/rename/generics/unions.go rename to gopls/internal/lsp/testdata/rename/generics/unions.go diff --git a/internal/lsp/testdata/rename/generics/unions.go.golden b/gopls/internal/lsp/testdata/rename/generics/unions.go.golden similarity index 100% rename from internal/lsp/testdata/rename/generics/unions.go.golden rename to gopls/internal/lsp/testdata/rename/generics/unions.go.golden diff --git a/internal/lsp/testdata/rename/issue39614/issue39614.go.golden b/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue39614/issue39614.go.golden rename to gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.golden diff --git a/internal/lsp/testdata/rename/issue39614/issue39614.go.in b/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.in similarity index 100% rename from internal/lsp/testdata/rename/issue39614/issue39614.go.in rename to gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.in diff --git a/internal/lsp/testdata/rename/issue42134/1.go b/gopls/internal/lsp/testdata/rename/issue42134/1.go similarity index 100% rename from internal/lsp/testdata/rename/issue42134/1.go rename to gopls/internal/lsp/testdata/rename/issue42134/1.go diff --git a/internal/lsp/testdata/rename/issue42134/1.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/1.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue42134/1.go.golden rename to gopls/internal/lsp/testdata/rename/issue42134/1.go.golden diff --git a/internal/lsp/testdata/rename/issue42134/2.go b/gopls/internal/lsp/testdata/rename/issue42134/2.go similarity index 100% rename from internal/lsp/testdata/rename/issue42134/2.go rename to gopls/internal/lsp/testdata/rename/issue42134/2.go diff --git a/internal/lsp/testdata/rename/issue42134/2.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/2.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue42134/2.go.golden rename to gopls/internal/lsp/testdata/rename/issue42134/2.go.golden diff --git a/internal/lsp/testdata/rename/issue42134/3.go b/gopls/internal/lsp/testdata/rename/issue42134/3.go similarity index 100% rename from internal/lsp/testdata/rename/issue42134/3.go rename to gopls/internal/lsp/testdata/rename/issue42134/3.go diff --git a/internal/lsp/testdata/rename/issue42134/3.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/3.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue42134/3.go.golden rename to gopls/internal/lsp/testdata/rename/issue42134/3.go.golden diff --git a/internal/lsp/testdata/rename/issue42134/4.go b/gopls/internal/lsp/testdata/rename/issue42134/4.go similarity index 100% rename from internal/lsp/testdata/rename/issue42134/4.go rename to gopls/internal/lsp/testdata/rename/issue42134/4.go diff --git a/internal/lsp/testdata/rename/issue42134/4.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/4.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue42134/4.go.golden rename to gopls/internal/lsp/testdata/rename/issue42134/4.go.golden diff --git a/internal/lsp/testdata/rename/issue43616/issue43616.go.golden b/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.golden similarity index 100% rename from internal/lsp/testdata/rename/issue43616/issue43616.go.golden rename to gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.golden diff --git a/internal/lsp/testdata/rename/issue43616/issue43616.go.in b/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.in similarity index 100% rename from internal/lsp/testdata/rename/issue43616/issue43616.go.in rename to gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.in diff --git a/internal/lsp/testdata/rename/shadow/shadow.go b/gopls/internal/lsp/testdata/rename/shadow/shadow.go similarity index 100% rename from internal/lsp/testdata/rename/shadow/shadow.go rename to gopls/internal/lsp/testdata/rename/shadow/shadow.go diff --git a/internal/lsp/testdata/rename/shadow/shadow.go.golden b/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden similarity index 100% rename from internal/lsp/testdata/rename/shadow/shadow.go.golden rename to gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden diff --git a/internal/lsp/testdata/rename/testy/testy.go b/gopls/internal/lsp/testdata/rename/testy/testy.go similarity index 100% rename from internal/lsp/testdata/rename/testy/testy.go rename to gopls/internal/lsp/testdata/rename/testy/testy.go diff --git a/internal/lsp/testdata/rename/testy/testy.go.golden b/gopls/internal/lsp/testdata/rename/testy/testy.go.golden similarity index 100% rename from internal/lsp/testdata/rename/testy/testy.go.golden rename to gopls/internal/lsp/testdata/rename/testy/testy.go.golden diff --git a/internal/lsp/testdata/rename/testy/testy_test.go b/gopls/internal/lsp/testdata/rename/testy/testy_test.go similarity index 100% rename from internal/lsp/testdata/rename/testy/testy_test.go rename to gopls/internal/lsp/testdata/rename/testy/testy_test.go diff --git a/internal/lsp/testdata/rename/testy/testy_test.go.golden b/gopls/internal/lsp/testdata/rename/testy/testy_test.go.golden similarity index 100% rename from internal/lsp/testdata/rename/testy/testy_test.go.golden rename to gopls/internal/lsp/testdata/rename/testy/testy_test.go.golden diff --git a/gopls/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go b/gopls/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go new file mode 100644 index 00000000000..783e9a55f17 --- /dev/null +++ b/gopls/internal/lsp/testdata/rundespiteerrors/rundespiteerrors.go @@ -0,0 +1,14 @@ +package rundespiteerrors + +// This test verifies that analyzers without RunDespiteErrors are not +// executed on a package containing type errors (see issue #54762). +func _() { + // A type error. + _ = 1 + "" //@diag("1", "compiler", "mismatched types|cannot convert", "error") + + // A violation of an analyzer for which RunDespiteErrors=false: + // no diagnostic is produced; the diag comment is merely illustrative. + for _ = range "" { //diag("for _", "simplifyrange", "simplify range expression", "warning") + + } +} diff --git a/internal/lsp/testdata/selector/selector.go.in b/gopls/internal/lsp/testdata/selector/selector.go.in similarity index 96% rename from internal/lsp/testdata/selector/selector.go.in rename to gopls/internal/lsp/testdata/selector/selector.go.in index 277f98bde7c..b1498a08c77 100644 --- a/internal/lsp/testdata/selector/selector.go.in +++ b/gopls/internal/lsp/testdata/selector/selector.go.in @@ -3,7 +3,7 @@ package selector import ( - "golang.org/x/tools/internal/lsp/bar" + "golang.org/lsptests/bar" ) type S struct { diff --git a/internal/lsp/testdata/semantic/README.md b/gopls/internal/lsp/testdata/semantic/README.md similarity index 100% rename from internal/lsp/testdata/semantic/README.md rename to gopls/internal/lsp/testdata/semantic/README.md diff --git a/internal/lsp/testdata/semantic/a.go b/gopls/internal/lsp/testdata/semantic/a.go similarity index 100% rename from internal/lsp/testdata/semantic/a.go rename to gopls/internal/lsp/testdata/semantic/a.go diff --git a/internal/lsp/testdata/semantic/a.go.golden b/gopls/internal/lsp/testdata/semantic/a.go.golden similarity index 95% rename from internal/lsp/testdata/semantic/a.go.golden rename to gopls/internal/lsp/testdata/semantic/a.go.golden index 19dd412407d..34b70e0f4f2 100644 --- a/internal/lsp/testdata/semantic/a.go.golden +++ b/gopls/internal/lsp/testdata/semantic/a.go.golden @@ -27,7 +27,7 @@ ) /*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/A /*⇒6,keyword,[]*/struct { - /*⇒1,variable,[definition]*/X /*⇒3,type,[defaultLibrary]*/int /*⇒6,comment,[]*/`foof` + /*⇒1,variable,[definition]*/X /*⇒3,type,[defaultLibrary]*/int /*⇒6,string,[]*/`foof` } /*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/B /*⇒9,keyword,[]*/interface { /*⇒1,type,[]*/A @@ -65,7 +65,7 @@ /*⇒2,variable,[definition]*/ff /*⇒2,operator,[]*/:= /*⇒4,keyword,[]*/func() {} /*⇒5,keyword,[]*/defer /*⇒2,variable,[]*/ff() /*⇒2,keyword,[]*/go /*⇒3,namespace,[]*/utf./*⇒9,function,[]*/RuneCount(/*⇒2,string,[]*/"") - /*⇒2,keyword,[]*/go /*⇒4,namespace,[]*/utf8./*⇒9,function,[]*/RuneCount(/*⇒2,variable,[]*/vv.(/*⇒6,type,[]*/string)) + /*⇒2,keyword,[]*/go /*⇒4,namespace,[]*/utf8./*⇒9,function,[]*/RuneCount(/*⇒2,parameter,[]*/vv.(/*⇒6,type,[]*/string)) /*⇒2,keyword,[]*/if /*⇒4,variable,[readonly]*/true { } /*⇒4,keyword,[]*/else { } @@ -73,9 +73,9 @@ /*⇒3,keyword,[]*/for /*⇒1,variable,[definition]*/i /*⇒2,operator,[]*/:= /*⇒1,number,[]*/0; /*⇒1,variable,[]*/i /*⇒1,operator,[]*/< /*⇒2,number,[]*/10; { /*⇒5,keyword,[]*/break Never } - _, /*⇒2,variable,[definition]*/ok /*⇒2,operator,[]*/:= /*⇒2,variable,[]*/vv[/*⇒1,number,[]*/0].(/*⇒1,type,[]*/A) + _, /*⇒2,variable,[definition]*/ok /*⇒2,operator,[]*/:= /*⇒2,parameter,[]*/vv[/*⇒1,number,[]*/0].(/*⇒1,type,[]*/A) /*⇒2,keyword,[]*/if /*⇒1,operator,[]*/!/*⇒2,variable,[]*/ok { - /*⇒6,keyword,[]*/switch /*⇒1,variable,[definition]*/x /*⇒2,operator,[]*/:= /*⇒2,variable,[]*/vv[/*⇒1,number,[]*/0].(/*⇒4,keyword,[]*/type) { + /*⇒6,keyword,[]*/switch /*⇒1,variable,[definition]*/x /*⇒2,operator,[]*/:= /*⇒2,parameter,[]*/vv[/*⇒1,number,[]*/0].(/*⇒4,keyword,[]*/type) { } /*⇒4,keyword,[]*/goto Never } diff --git a/internal/lsp/testdata/semantic/b.go b/gopls/internal/lsp/testdata/semantic/b.go similarity index 100% rename from internal/lsp/testdata/semantic/b.go rename to gopls/internal/lsp/testdata/semantic/b.go diff --git a/internal/lsp/testdata/semantic/b.go.golden b/gopls/internal/lsp/testdata/semantic/b.go.golden similarity index 100% rename from internal/lsp/testdata/semantic/b.go.golden rename to gopls/internal/lsp/testdata/semantic/b.go.golden diff --git a/internal/lsp/testdata/semantic/semantic_test.go b/gopls/internal/lsp/testdata/semantic/semantic_test.go similarity index 100% rename from internal/lsp/testdata/semantic/semantic_test.go rename to gopls/internal/lsp/testdata/semantic/semantic_test.go diff --git a/internal/lsp/testdata/signature/signature.go b/gopls/internal/lsp/testdata/signature/signature.go similarity index 100% rename from internal/lsp/testdata/signature/signature.go rename to gopls/internal/lsp/testdata/signature/signature.go diff --git a/internal/lsp/testdata/signature/signature.go.golden b/gopls/internal/lsp/testdata/signature/signature.go.golden similarity index 86% rename from internal/lsp/testdata/signature/signature.go.golden rename to gopls/internal/lsp/testdata/signature/signature.go.golden index d7a65b3b873..90a4facf9a7 100644 --- a/internal/lsp/testdata/signature/signature.go.golden +++ b/gopls/internal/lsp/testdata/signature/signature.go.golden @@ -10,12 +10,6 @@ Bar(float64, ...byte) -- Foo(a string, b int) (c bool)-signature -- Foo(a string, b int) (c bool) --- GetAlias() Alias-signature -- -GetAlias() Alias - --- GetAliasPtr() *Alias-signature -- -GetAliasPtr() *Alias - -- Next(n int) []byte-signature -- Next(n int) []byte @@ -24,12 +18,6 @@ Next returns a slice containing the next n bytes from the buffer, advancing the -- OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias-signature -- OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias --- SetAliasSlice(a []*Alias)-signature -- -SetAliasSlice(a []*Alias) - --- SetOtherAliasMap(a map[*Alias]OtherAlias)-signature -- -SetOtherAliasMap(a map[*Alias]OtherAlias) - -- fn(hi string, there string) func(i int) rune-signature -- fn(hi string, there string) func(i int) rune diff --git a/internal/lsp/testdata/signature/signature2.go.golden b/gopls/internal/lsp/testdata/signature/signature2.go.golden similarity index 100% rename from internal/lsp/testdata/signature/signature2.go.golden rename to gopls/internal/lsp/testdata/signature/signature2.go.golden diff --git a/internal/lsp/testdata/signature/signature2.go.in b/gopls/internal/lsp/testdata/signature/signature2.go.in similarity index 100% rename from internal/lsp/testdata/signature/signature2.go.in rename to gopls/internal/lsp/testdata/signature/signature2.go.in diff --git a/internal/lsp/testdata/signature/signature3.go.golden b/gopls/internal/lsp/testdata/signature/signature3.go.golden similarity index 100% rename from internal/lsp/testdata/signature/signature3.go.golden rename to gopls/internal/lsp/testdata/signature/signature3.go.golden diff --git a/internal/lsp/testdata/signature/signature3.go.in b/gopls/internal/lsp/testdata/signature/signature3.go.in similarity index 100% rename from internal/lsp/testdata/signature/signature3.go.in rename to gopls/internal/lsp/testdata/signature/signature3.go.in diff --git a/internal/lsp/testdata/signature/signature_test.go b/gopls/internal/lsp/testdata/signature/signature_test.go similarity index 90% rename from internal/lsp/testdata/signature/signature_test.go rename to gopls/internal/lsp/testdata/signature/signature_test.go index 62e54a23834..500247dbdec 100644 --- a/internal/lsp/testdata/signature/signature_test.go +++ b/gopls/internal/lsp/testdata/signature/signature_test.go @@ -3,7 +3,7 @@ package signature_test import ( "testing" - sig "golang.org/x/tools/internal/lsp/signature" + sig "golang.org/lsptests/signature" ) func TestSignature(t *testing.T) { diff --git a/gopls/internal/lsp/testdata/signature/signature_test.go.golden b/gopls/internal/lsp/testdata/signature/signature_test.go.golden new file mode 100644 index 00000000000..9e6561ac529 --- /dev/null +++ b/gopls/internal/lsp/testdata/signature/signature_test.go.golden @@ -0,0 +1,9 @@ +-- AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias)-signature -- +AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias) + +-- AliasSlice(a []*sig.Alias) (b sig.Alias)-signature -- +AliasSlice(a []*sig.Alias) (b sig.Alias) + +-- OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias-signature -- +OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias + diff --git a/internal/lsp/testdata/snippets/func_snippets118.go.in b/gopls/internal/lsp/testdata/snippets/func_snippets118.go.in similarity index 100% rename from internal/lsp/testdata/snippets/func_snippets118.go.in rename to gopls/internal/lsp/testdata/snippets/func_snippets118.go.in diff --git a/internal/lsp/testdata/snippets/literal.go b/gopls/internal/lsp/testdata/snippets/literal.go similarity index 84% rename from internal/lsp/testdata/snippets/literal.go rename to gopls/internal/lsp/testdata/snippets/literal.go index 43931d18ef7..fbb642f08a5 100644 --- a/internal/lsp/testdata/snippets/literal.go +++ b/gopls/internal/lsp/testdata/snippets/literal.go @@ -1,8 +1,8 @@ package snippets import ( - "golang.org/x/tools/internal/lsp/signature" - t "golang.org/x/tools/internal/lsp/types" + "golang.org/lsptests/signature" + t "golang.org/lsptests/types" ) type structy struct { diff --git a/gopls/internal/lsp/testdata/snippets/literal.go.golden b/gopls/internal/lsp/testdata/snippets/literal.go.golden new file mode 100644 index 00000000000..c91e5e9e086 --- /dev/null +++ b/gopls/internal/lsp/testdata/snippets/literal.go.golden @@ -0,0 +1,3 @@ +-- X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias-signature -- +X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias + diff --git a/internal/lsp/testdata/snippets/literal_snippets.go.in b/gopls/internal/lsp/testdata/snippets/literal_snippets.go.in similarity index 99% rename from internal/lsp/testdata/snippets/literal_snippets.go.in rename to gopls/internal/lsp/testdata/snippets/literal_snippets.go.in index 4a2a01dfa1f..c6e6c0fbd60 100644 --- a/internal/lsp/testdata/snippets/literal_snippets.go.in +++ b/gopls/internal/lsp/testdata/snippets/literal_snippets.go.in @@ -7,7 +7,7 @@ import ( "net/http" "sort" - "golang.org/x/tools/internal/lsp/foo" + "golang.org/lsptests/foo" ) func _() { diff --git a/internal/lsp/testdata/snippets/literal_snippets118.go.in b/gopls/internal/lsp/testdata/snippets/literal_snippets118.go.in similarity index 100% rename from internal/lsp/testdata/snippets/literal_snippets118.go.in rename to gopls/internal/lsp/testdata/snippets/literal_snippets118.go.in diff --git a/internal/lsp/testdata/snippets/postfix.go b/gopls/internal/lsp/testdata/snippets/postfix.go similarity index 100% rename from internal/lsp/testdata/snippets/postfix.go rename to gopls/internal/lsp/testdata/snippets/postfix.go diff --git a/internal/lsp/testdata/snippets/snippets.go.golden b/gopls/internal/lsp/testdata/snippets/snippets.go.golden similarity index 100% rename from internal/lsp/testdata/snippets/snippets.go.golden rename to gopls/internal/lsp/testdata/snippets/snippets.go.golden diff --git a/internal/lsp/testdata/snippets/snippets.go.in b/gopls/internal/lsp/testdata/snippets/snippets.go.in similarity index 100% rename from internal/lsp/testdata/snippets/snippets.go.in rename to gopls/internal/lsp/testdata/snippets/snippets.go.in diff --git a/internal/lsp/testdata/statements/append.go b/gopls/internal/lsp/testdata/statements/append.go similarity index 100% rename from internal/lsp/testdata/statements/append.go rename to gopls/internal/lsp/testdata/statements/append.go diff --git a/internal/lsp/testdata/statements/if_err_check_return.go b/gopls/internal/lsp/testdata/statements/if_err_check_return.go similarity index 100% rename from internal/lsp/testdata/statements/if_err_check_return.go rename to gopls/internal/lsp/testdata/statements/if_err_check_return.go diff --git a/internal/lsp/testdata/statements/if_err_check_return_2.go b/gopls/internal/lsp/testdata/statements/if_err_check_return_2.go similarity index 100% rename from internal/lsp/testdata/statements/if_err_check_return_2.go rename to gopls/internal/lsp/testdata/statements/if_err_check_return_2.go diff --git a/internal/lsp/testdata/statements/if_err_check_test.go b/gopls/internal/lsp/testdata/statements/if_err_check_test.go similarity index 100% rename from internal/lsp/testdata/statements/if_err_check_test.go rename to gopls/internal/lsp/testdata/statements/if_err_check_test.go diff --git a/internal/lsp/testdata/stub/other/other.go b/gopls/internal/lsp/testdata/stub/other/other.go similarity index 100% rename from internal/lsp/testdata/stub/other/other.go rename to gopls/internal/lsp/testdata/stub/other/other.go diff --git a/internal/lsp/testdata/stub/stub_add_selector.go b/gopls/internal/lsp/testdata/stub/stub_add_selector.go similarity index 92% rename from internal/lsp/testdata/stub/stub_add_selector.go rename to gopls/internal/lsp/testdata/stub/stub_add_selector.go index a15afd7c244..4037b7ad3a0 100644 --- a/internal/lsp/testdata/stub/stub_add_selector.go +++ b/gopls/internal/lsp/testdata/stub/stub_add_selector.go @@ -7,6 +7,6 @@ import "io" // then our implementation must add the import/package selector // in the concrete method if the concrete type is outside of the interface // package -var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "refactor.rewrite") +var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "refactor.rewrite", "") type readerFrom struct{} diff --git a/internal/lsp/testdata/stub/stub_add_selector.go.golden b/gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden similarity index 95% rename from internal/lsp/testdata/stub/stub_add_selector.go.golden rename to gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden index e885483eaaf..8f08ca1efe2 100644 --- a/internal/lsp/testdata/stub/stub_add_selector.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden @@ -8,7 +8,7 @@ import "io" // then our implementation must add the import/package selector // in the concrete method if the concrete type is outside of the interface // package -var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "refactor.rewrite") +var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "refactor.rewrite", "") type readerFrom struct{} diff --git a/internal/lsp/testdata/stub/stub_assign.go b/gopls/internal/lsp/testdata/stub/stub_assign.go similarity index 58% rename from internal/lsp/testdata/stub/stub_assign.go rename to gopls/internal/lsp/testdata/stub/stub_assign.go index 9336361d009..d3f09313f25 100644 --- a/internal/lsp/testdata/stub/stub_assign.go +++ b/gopls/internal/lsp/testdata/stub/stub_assign.go @@ -4,7 +4,7 @@ import "io" func main() { var br io.ByteWriter - br = &byteWriter{} //@suggestedfix("&", "refactor.rewrite") + br = &byteWriter{} //@suggestedfix("&", "refactor.rewrite", "") } type byteWriter struct{} diff --git a/internal/lsp/testdata/stub/stub_assign.go.golden b/gopls/internal/lsp/testdata/stub/stub_assign.go.golden similarity index 78% rename from internal/lsp/testdata/stub/stub_assign.go.golden rename to gopls/internal/lsp/testdata/stub/stub_assign.go.golden index a52a8236798..f1535424114 100644 --- a/internal/lsp/testdata/stub/stub_assign.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_assign.go.golden @@ -5,7 +5,7 @@ import "io" func main() { var br io.ByteWriter - br = &byteWriter{} //@suggestedfix("&", "refactor.rewrite") + br = &byteWriter{} //@suggestedfix("&", "refactor.rewrite", "") } type byteWriter struct{} diff --git a/internal/lsp/testdata/stub/stub_assign_multivars.go b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go similarity index 93% rename from internal/lsp/testdata/stub/stub_assign_multivars.go rename to gopls/internal/lsp/testdata/stub/stub_assign_multivars.go index 01b330fda54..bd36d6833d1 100644 --- a/internal/lsp/testdata/stub/stub_assign_multivars.go +++ b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go @@ -5,7 +5,7 @@ import "io" func main() { var br io.ByteWriter var i int - i, br = 1, &multiByteWriter{} //@suggestedfix("&", "refactor.rewrite") + i, br = 1, &multiByteWriter{} //@suggestedfix("&", "refactor.rewrite", "") } type multiByteWriter struct{} diff --git a/internal/lsp/testdata/stub/stub_assign_multivars.go.golden b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden similarity index 96% rename from internal/lsp/testdata/stub/stub_assign_multivars.go.golden rename to gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden index e1e71adbd50..425d11746a5 100644 --- a/internal/lsp/testdata/stub/stub_assign_multivars.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden @@ -6,7 +6,7 @@ import "io" func main() { var br io.ByteWriter var i int - i, br = 1, &multiByteWriter{} //@suggestedfix("&", "refactor.rewrite") + i, br = 1, &multiByteWriter{} //@suggestedfix("&", "refactor.rewrite", "") } type multiByteWriter struct{} diff --git a/internal/lsp/testdata/stub/stub_call_expr.go b/gopls/internal/lsp/testdata/stub/stub_call_expr.go similarity index 63% rename from internal/lsp/testdata/stub/stub_call_expr.go rename to gopls/internal/lsp/testdata/stub/stub_call_expr.go index 775b0e5545e..0c309466524 100644 --- a/internal/lsp/testdata/stub/stub_call_expr.go +++ b/gopls/internal/lsp/testdata/stub/stub_call_expr.go @@ -1,7 +1,7 @@ package stub func main() { - check(&callExpr{}) //@suggestedfix("&", "refactor.rewrite") + check(&callExpr{}) //@suggestedfix("&", "refactor.rewrite", "") } func check(err error) { diff --git a/internal/lsp/testdata/stub/stub_call_expr.go.golden b/gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden similarity index 78% rename from internal/lsp/testdata/stub/stub_call_expr.go.golden rename to gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden index 2d12f8651f3..c82d22440f1 100644 --- a/internal/lsp/testdata/stub/stub_call_expr.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden @@ -2,7 +2,7 @@ package stub func main() { - check(&callExpr{}) //@suggestedfix("&", "refactor.rewrite") + check(&callExpr{}) //@suggestedfix("&", "refactor.rewrite", "") } func check(err error) { diff --git a/internal/lsp/testdata/stub/stub_embedded.go b/gopls/internal/lsp/testdata/stub/stub_embedded.go similarity index 86% rename from internal/lsp/testdata/stub/stub_embedded.go rename to gopls/internal/lsp/testdata/stub/stub_embedded.go index 6d6a986bf24..f66989e9f0f 100644 --- a/internal/lsp/testdata/stub/stub_embedded.go +++ b/gopls/internal/lsp/testdata/stub/stub_embedded.go @@ -5,7 +5,7 @@ import ( "sort" ) -var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "refactor.rewrite") +var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "refactor.rewrite", "") type embeddedConcrete struct{} diff --git a/internal/lsp/testdata/stub/stub_embedded.go.golden b/gopls/internal/lsp/testdata/stub/stub_embedded.go.golden similarity index 95% rename from internal/lsp/testdata/stub/stub_embedded.go.golden rename to gopls/internal/lsp/testdata/stub/stub_embedded.go.golden index c258ebaf46c..3c5347e8c01 100644 --- a/internal/lsp/testdata/stub/stub_embedded.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_embedded.go.golden @@ -6,7 +6,7 @@ import ( "sort" ) -var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "refactor.rewrite") +var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "refactor.rewrite", "") type embeddedConcrete struct{} diff --git a/internal/lsp/testdata/stub/stub_err.go b/gopls/internal/lsp/testdata/stub/stub_err.go similarity index 92% rename from internal/lsp/testdata/stub/stub_err.go rename to gopls/internal/lsp/testdata/stub/stub_err.go index 908c7d3152f..121f0e794d7 100644 --- a/internal/lsp/testdata/stub/stub_err.go +++ b/gopls/internal/lsp/testdata/stub/stub_err.go @@ -1,7 +1,7 @@ package stub func main() { - var br error = &customErr{} //@suggestedfix("&", "refactor.rewrite") + var br error = &customErr{} //@suggestedfix("&", "refactor.rewrite", "") } type customErr struct{} diff --git a/internal/lsp/testdata/stub/stub_err.go.golden b/gopls/internal/lsp/testdata/stub/stub_err.go.golden similarity index 96% rename from internal/lsp/testdata/stub/stub_err.go.golden rename to gopls/internal/lsp/testdata/stub/stub_err.go.golden index 717aed86293..0b441bdaab1 100644 --- a/internal/lsp/testdata/stub/stub_err.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_err.go.golden @@ -2,7 +2,7 @@ package stub func main() { - var br error = &customErr{} //@suggestedfix("&", "refactor.rewrite") + var br error = &customErr{} //@suggestedfix("&", "refactor.rewrite", "") } type customErr struct{} diff --git a/internal/lsp/testdata/stub/stub_function_return.go b/gopls/internal/lsp/testdata/stub/stub_function_return.go similarity index 57% rename from internal/lsp/testdata/stub/stub_function_return.go rename to gopls/internal/lsp/testdata/stub/stub_function_return.go index bbf05885af2..41f17645e9c 100644 --- a/internal/lsp/testdata/stub/stub_function_return.go +++ b/gopls/internal/lsp/testdata/stub/stub_function_return.go @@ -5,7 +5,7 @@ import ( ) func newCloser() io.Closer { - return closer{} //@suggestedfix("c", "refactor.rewrite") + return closer{} //@suggestedfix("c", "refactor.rewrite", "") } type closer struct{} diff --git a/internal/lsp/testdata/stub/stub_function_return.go.golden b/gopls/internal/lsp/testdata/stub/stub_function_return.go.golden similarity index 77% rename from internal/lsp/testdata/stub/stub_function_return.go.golden rename to gopls/internal/lsp/testdata/stub/stub_function_return.go.golden index f80874d2b94..e90712e6973 100644 --- a/internal/lsp/testdata/stub/stub_function_return.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_function_return.go.golden @@ -6,7 +6,7 @@ import ( ) func newCloser() io.Closer { - return closer{} //@suggestedfix("c", "refactor.rewrite") + return closer{} //@suggestedfix("c", "refactor.rewrite", "") } type closer struct{} diff --git a/internal/lsp/testdata/stub/stub_generic_receiver.go b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go similarity index 82% rename from internal/lsp/testdata/stub/stub_generic_receiver.go rename to gopls/internal/lsp/testdata/stub/stub_generic_receiver.go index 64e90fcf6a7..1c00569ea1c 100644 --- a/internal/lsp/testdata/stub/stub_generic_receiver.go +++ b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go @@ -7,7 +7,7 @@ import "io" // This file tests that that the stub method generator accounts for concrete // types that have type parameters defined. -var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite") +var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite", "Implement io.ReaderFrom") type genReader[T, Y any] struct { T T diff --git a/internal/lsp/testdata/stub/stub_generic_receiver.go.golden b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden similarity index 88% rename from internal/lsp/testdata/stub/stub_generic_receiver.go.golden rename to gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden index 1fc7157b463..97935d47eb3 100644 --- a/internal/lsp/testdata/stub/stub_generic_receiver.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden @@ -8,7 +8,7 @@ import "io" // This file tests that that the stub method generator accounts for concrete // types that have type parameters defined. -var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite") +var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite", "Implement io.ReaderFrom") type genReader[T, Y any] struct { T T diff --git a/internal/lsp/testdata/stub/stub_ignored_imports.go b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go similarity index 92% rename from internal/lsp/testdata/stub/stub_ignored_imports.go rename to gopls/internal/lsp/testdata/stub/stub_ignored_imports.go index 8f6ec73de1b..ca95d2a7120 100644 --- a/internal/lsp/testdata/stub/stub_ignored_imports.go +++ b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go @@ -12,7 +12,7 @@ import ( var ( _ Reader - _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "refactor.rewrite") + _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "refactor.rewrite", "") ) type ignoredResetter struct{} diff --git a/internal/lsp/testdata/stub/stub_ignored_imports.go.golden b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden similarity index 95% rename from internal/lsp/testdata/stub/stub_ignored_imports.go.golden rename to gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden index a0ddc179353..33aba532662 100644 --- a/internal/lsp/testdata/stub/stub_ignored_imports.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden @@ -14,7 +14,7 @@ import ( var ( _ Reader - _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "refactor.rewrite") + _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "refactor.rewrite", "") ) type ignoredResetter struct{} diff --git a/internal/lsp/testdata/stub/stub_multi_var.go b/gopls/internal/lsp/testdata/stub/stub_multi_var.go similarity index 89% rename from internal/lsp/testdata/stub/stub_multi_var.go rename to gopls/internal/lsp/testdata/stub/stub_multi_var.go index 4276b799429..06702b22204 100644 --- a/internal/lsp/testdata/stub/stub_multi_var.go +++ b/gopls/internal/lsp/testdata/stub/stub_multi_var.go @@ -6,6 +6,6 @@ import "io" // has multiple values on the same line can still be // analyzed correctly to target the interface implementation // diagnostic. -var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "refactor.rewrite") +var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "refactor.rewrite", "") type multiVar struct{} diff --git a/internal/lsp/testdata/stub/stub_multi_var.go.golden b/gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden similarity index 92% rename from internal/lsp/testdata/stub/stub_multi_var.go.golden rename to gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden index b9ac4236766..804c7eec65c 100644 --- a/internal/lsp/testdata/stub/stub_multi_var.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden @@ -7,7 +7,7 @@ import "io" // has multiple values on the same line can still be // analyzed correctly to target the interface implementation // diagnostic. -var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "refactor.rewrite") +var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "refactor.rewrite", "") type multiVar struct{} diff --git a/internal/lsp/testdata/stub/stub_pointer.go b/gopls/internal/lsp/testdata/stub/stub_pointer.go similarity index 57% rename from internal/lsp/testdata/stub/stub_pointer.go rename to gopls/internal/lsp/testdata/stub/stub_pointer.go index 2b3681b8357..e9d8bc688fc 100644 --- a/internal/lsp/testdata/stub/stub_pointer.go +++ b/gopls/internal/lsp/testdata/stub/stub_pointer.go @@ -3,7 +3,7 @@ package stub import "io" func getReaderFrom() io.ReaderFrom { - return &pointerImpl{} //@suggestedfix("&", "refactor.rewrite") + return &pointerImpl{} //@suggestedfix("&", "refactor.rewrite", "") } type pointerImpl struct{} diff --git a/internal/lsp/testdata/stub/stub_pointer.go.golden b/gopls/internal/lsp/testdata/stub/stub_pointer.go.golden similarity index 79% rename from internal/lsp/testdata/stub/stub_pointer.go.golden rename to gopls/internal/lsp/testdata/stub/stub_pointer.go.golden index c4133d7a44d..a4d765dd457 100644 --- a/internal/lsp/testdata/stub/stub_pointer.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_pointer.go.golden @@ -4,7 +4,7 @@ package stub import "io" func getReaderFrom() io.ReaderFrom { - return &pointerImpl{} //@suggestedfix("&", "refactor.rewrite") + return &pointerImpl{} //@suggestedfix("&", "refactor.rewrite", "") } type pointerImpl struct{} diff --git a/internal/lsp/testdata/stub/stub_renamed_import.go b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go similarity index 93% rename from internal/lsp/testdata/stub/stub_renamed_import.go rename to gopls/internal/lsp/testdata/stub/stub_renamed_import.go index eaebe251018..54dd598013d 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import.go +++ b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go @@ -5,7 +5,7 @@ import ( myio "io" ) -var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "refactor.rewrite") +var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "refactor.rewrite", "") var _ myio.Reader type myIO struct{} diff --git a/internal/lsp/testdata/stub/stub_renamed_import.go.golden b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden similarity index 96% rename from internal/lsp/testdata/stub/stub_renamed_import.go.golden rename to gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden index 48ff4f1537f..8182d2b3675 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden @@ -6,7 +6,7 @@ import ( myio "io" ) -var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "refactor.rewrite") +var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "refactor.rewrite", "") var _ myio.Reader type myIO struct{} diff --git a/internal/lsp/testdata/stub/stub_renamed_import_iface.go b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go similarity index 80% rename from internal/lsp/testdata/stub/stub_renamed_import_iface.go rename to gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go index 96caf540d60..0f175868504 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import_iface.go +++ b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go @@ -1,13 +1,13 @@ package stub import ( - "golang.org/x/tools/internal/lsp/stub/other" + "golang.org/lsptests/stub/other" ) // This file tests that if an interface // method references an import from its own package // that the concrete type does not yet import, and that import happens // to be renamed, then we prefer the renaming of the interface. -var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "refactor.rewrite") +var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "refactor.rewrite", "") type otherInterfaceImpl struct{} diff --git a/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden similarity index 86% rename from internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden rename to gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden index 9ba2cb440e8..3d6ac0a551c 100644 --- a/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden @@ -4,14 +4,14 @@ package stub import ( "bytes" renamed_context "context" - "golang.org/x/tools/internal/lsp/stub/other" + "golang.org/lsptests/stub/other" ) // This file tests that if an interface // method references an import from its own package // that the concrete type does not yet import, and that import happens // to be renamed, then we prefer the renaming of the interface. -var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "refactor.rewrite") +var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "refactor.rewrite", "") type otherInterfaceImpl struct{} diff --git a/internal/lsp/testdata/stub/stub_stdlib.go b/gopls/internal/lsp/testdata/stub/stub_stdlib.go similarity index 93% rename from internal/lsp/testdata/stub/stub_stdlib.go rename to gopls/internal/lsp/testdata/stub/stub_stdlib.go index 0d54a6daadf..463cf78a344 100644 --- a/internal/lsp/testdata/stub/stub_stdlib.go +++ b/gopls/internal/lsp/testdata/stub/stub_stdlib.go @@ -4,6 +4,6 @@ import ( "io" ) -var _ io.Writer = writer{} //@suggestedfix("w", "refactor.rewrite") +var _ io.Writer = writer{} //@suggestedfix("w", "refactor.rewrite", "") type writer struct{} diff --git a/internal/lsp/testdata/stub/stub_stdlib.go.golden b/gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden similarity index 97% rename from internal/lsp/testdata/stub/stub_stdlib.go.golden rename to gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden index 8636cead414..55592501a07 100644 --- a/internal/lsp/testdata/stub/stub_stdlib.go.golden +++ b/gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden @@ -5,7 +5,7 @@ import ( "io" ) -var _ io.Writer = writer{} //@suggestedfix("w", "refactor.rewrite") +var _ io.Writer = writer{} //@suggestedfix("w", "refactor.rewrite", "") type writer struct{} diff --git a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go b/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go similarity index 65% rename from internal/lsp/testdata/suggestedfix/has_suggested_fix.go rename to gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go index e06dce0a846..7ff524479b4 100644 --- a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go +++ b/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go @@ -6,6 +6,6 @@ import ( func goodbye() { s := "hiiiiiii" - s = s //@suggestedfix("s = s", "quickfix") + s = s //@suggestedfix("s = s", "quickfix", "") log.Print(s) } diff --git a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden b/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden similarity index 75% rename from internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden rename to gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden index 9ccaa199468..e7e84fc227d 100644 --- a/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden +++ b/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden @@ -7,7 +7,7 @@ import ( func goodbye() { s := "hiiiiiii" - //@suggestedfix("s = s", "quickfix") + //@suggestedfix("s = s", "quickfix", "") log.Print(s) } diff --git a/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden similarity index 76% rename from internal/lsp/testdata/summary.txt.golden rename to gopls/internal/lsp/testdata/summary.txt.golden index 9e1d84d1d56..cfe8e4a267d 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,28 +1,29 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 265 +CompletionsCount = 263 CompletionSnippetCount = 106 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 -RankedCompletionsCount = 163 +RankedCompletionsCount = 164 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 37 +DiagnosticsCount = 39 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 SemanticTokenCount = 3 SuggestedFixCount = 63 -FunctionExtractionCount = 25 +FunctionExtractionCount = 27 MethodExtractionCount = 6 -DefinitionsCount = 95 +DefinitionsCount = 99 TypeDefinitionsCount = 18 HighlightsCount = 69 +InlayHintsCount = 4 ReferencesCount = 27 RenamesCount = 41 PrepareRenamesCount = 7 -SymbolsCount = 5 +SymbolsCount = 1 WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 diff --git a/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden similarity index 68% rename from internal/lsp/testdata/summary_go1.18.txt.golden rename to gopls/internal/lsp/testdata/summary_go1.18.txt.golden index 1c6ad922c36..2b7bf976b2f 100644 --- a/internal/lsp/testdata/summary_go1.18.txt.golden +++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden @@ -1,28 +1,29 @@ -- summary -- CallHierarchyCount = 2 CodeLensCount = 5 -CompletionsCount = 266 -CompletionSnippetCount = 116 +CompletionsCount = 264 +CompletionSnippetCount = 115 UnimportedCompletionsCount = 5 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 -RankedCompletionsCount = 173 +RankedCompletionsCount = 174 CaseSensitiveCompletionsCount = 4 -DiagnosticsCount = 37 +DiagnosticsCount = 39 FoldingRangesCount = 2 FormatCount = 6 ImportCount = 8 SemanticTokenCount = 3 -SuggestedFixCount = 64 -FunctionExtractionCount = 25 +SuggestedFixCount = 69 +FunctionExtractionCount = 27 MethodExtractionCount = 6 -DefinitionsCount = 108 +DefinitionsCount = 110 TypeDefinitionsCount = 18 HighlightsCount = 69 +InlayHintsCount = 5 ReferencesCount = 27 RenamesCount = 48 PrepareRenamesCount = 7 -SymbolsCount = 5 +SymbolsCount = 2 WorkspaceSymbolsCount = 20 SignaturesCount = 33 LinksCount = 7 diff --git a/gopls/internal/lsp/testdata/symbols/go1.18.go b/gopls/internal/lsp/testdata/symbols/go1.18.go new file mode 100644 index 00000000000..cdf99dc20ff --- /dev/null +++ b/gopls/internal/lsp/testdata/symbols/go1.18.go @@ -0,0 +1,16 @@ +//go:build go1.18 +// +build go1.18 + +package main + +type T[P any] struct { //@symbol("T", "T", "Struct", "struct{...}", "T", "") + F P //@symbol("F", "F", "Field", "P", "", "T") +} + +type Constraint interface { //@symbol("Constraint", "Constraint", "Interface", "interface{...}", "Constraint", "") + ~int | struct{ int } //@symbol("~int | struct{int}", "~int | struct{ int }", "Field", "", "", "Constraint") + + // TODO(rfindley): the selection range below is the entire interface field. + // Can we reduce it? + interface{ M() } //@symbol("interface{...}", "interface{ M() }", "Field", "", "iFaceField", "Constraint"), symbol("M", "M", "Method", "func()", "", "iFaceField") +} diff --git a/gopls/internal/lsp/testdata/symbols/go1.18.go.golden b/gopls/internal/lsp/testdata/symbols/go1.18.go.golden new file mode 100644 index 00000000000..5a0c1a94d7a --- /dev/null +++ b/gopls/internal/lsp/testdata/symbols/go1.18.go.golden @@ -0,0 +1,7 @@ +-- symbols -- +T Struct 6:6-6:7 + F Field 7:2-7:3 +Constraint Interface 10:6-10:16 + interface{...} Field 15:2-15:18 + ~int | struct{int} Field 11:2-11:22 + diff --git a/gopls/internal/lsp/testdata/symbols/main.go b/gopls/internal/lsp/testdata/symbols/main.go new file mode 100644 index 00000000000..65e0869fd5f --- /dev/null +++ b/gopls/internal/lsp/testdata/symbols/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "io" +) + +// Each symbol marker in this file defines the following information: +// symbol(name, selectionSpan, kind, detail, id, parentID) +// - name: DocumentSymbol.Name +// - selectionSpan: DocumentSymbol.SelectionRange +// - kind: DocumentSymbol.Kind +// - detail: DocumentSymbol.Detail +// - id: if non-empty, a unique identifier for this symbol +// - parentID: if non-empty, the id of the parent of this symbol +// +// This data in aggregate defines a set of document symbols and their +// parent-child relationships, which is compared against the DocummentSymbols +// response from gopls for the current file. +// +// TODO(rfindley): the symbol annotations here are complicated and difficult to +// maintain. It would be simpler to just write out the full expected response +// in the golden file, perhaps as raw JSON. + +var _ = 1 + +var x = 42 //@symbol("x", "x", "Variable", "", "", "") + +var nested struct { //@symbol("nested", "nested", "Variable", "struct{...}", "nested", "") + nestedField struct { //@symbol("nestedField", "nestedField", "Field", "struct{...}", "nestedField", "nested") + f int //@symbol("f", "f", "Field", "int", "", "nestedField") + } +} + +const y = 43 //@symbol("y", "y", "Constant", "", "", "") + +type Number int //@symbol("Number", "Number", "Class", "int", "", "") + +type Alias = string //@symbol("Alias", "Alias", "Class", "string", "", "") + +type NumberAlias = Number //@symbol("NumberAlias", "NumberAlias", "Class", "Number", "", "") + +type ( + Boolean bool //@symbol("Boolean", "Boolean", "Class", "bool", "", "") + BoolAlias = bool //@symbol("BoolAlias", "BoolAlias", "Class", "bool", "", "") +) + +type Foo struct { //@symbol("Foo", "Foo", "Struct", "struct{...}", "Foo", "") + Quux //@symbol("Quux", "Quux", "Field", "Quux", "", "Foo") + W io.Writer //@symbol("W", "W", "Field", "io.Writer", "", "Foo") + Bar int //@symbol("Bar", "Bar", "Field", "int", "", "Foo") + baz string //@symbol("baz", "baz", "Field", "string", "", "Foo") + funcField func(int) int //@symbol("funcField", "funcField", "Field", "func(int) int", "", "Foo") +} + +type Quux struct { //@symbol("Quux", "Quux", "Struct", "struct{...}", "Quux", "") + X, Y float64 //@symbol("X", "X", "Field", "float64", "", "Quux"), symbol("Y", "Y", "Field", "float64", "", "Quux") +} + +type EmptyStruct struct{} //@symbol("EmptyStruct", "EmptyStruct", "Struct", "struct{}", "", "") + +func (f Foo) Baz() string { //@symbol("(Foo).Baz", "Baz", "Method", "func() string", "", "") + return f.baz +} + +func _() {} + +func (q *Quux) Do() {} //@symbol("(*Quux).Do", "Do", "Method", "func()", "", "") + +func main() { //@symbol("main", "main", "Function", "func()", "", "") +} + +type Stringer interface { //@symbol("Stringer", "Stringer", "Interface", "interface{...}", "Stringer", "") + String() string //@symbol("String", "String", "Method", "func() string", "", "Stringer") +} + +type ABer interface { //@symbol("ABer", "ABer", "Interface", "interface{...}", "ABer", "") + B() //@symbol("B", "B", "Method", "func()", "", "ABer") + A() string //@symbol("A", "A", "Method", "func() string", "", "ABer") +} + +type WithEmbeddeds interface { //@symbol("WithEmbeddeds", "WithEmbeddeds", "Interface", "interface{...}", "WithEmbeddeds", "") + Do() //@symbol("Do", "Do", "Method", "func()", "", "WithEmbeddeds") + ABer //@symbol("ABer", "ABer", "Field", "ABer", "", "WithEmbeddeds") + io.Writer //@symbol("Writer", "Writer", "Field", "io.Writer", "", "WithEmbeddeds") +} + +type EmptyInterface interface{} //@symbol("EmptyInterface", "EmptyInterface", "Interface", "interface{}", "", "") + +func Dunk() int { return 0 } //@symbol("Dunk", "Dunk", "Function", "func() int", "", "") + +func dunk() {} //@symbol("dunk", "dunk", "Function", "func()", "", "") diff --git a/gopls/internal/lsp/testdata/symbols/main.go.golden b/gopls/internal/lsp/testdata/symbols/main.go.golden new file mode 100644 index 00000000000..98009b02d68 --- /dev/null +++ b/gopls/internal/lsp/testdata/symbols/main.go.golden @@ -0,0 +1,36 @@ +-- symbols -- +x Variable 26:5-26:6 +nested Variable 28:5-28:11 + nestedField Field 29:2-29:13 +y Constant 34:7-34:8 +Number Class 36:6-36:12 +Alias Class 38:6-38:11 +NumberAlias Class 40:6-40:17 +Boolean Class 43:2-43:9 +BoolAlias Class 44:2-44:11 +Foo Struct 47:6-47:9 + Bar Field 50:2-50:5 + Quux Field 48:2-48:6 + W Field 49:2-49:3 + baz Field 51:2-51:5 + funcField Field 52:2-52:11 +Quux Struct 55:6-55:10 + X Field 56:2-56:3 + Y Field 56:5-56:6 +EmptyStruct Struct 59:6-59:17 +(Foo).Baz Method 61:14-61:17 +(*Quux).Do Method 67:16-67:18 +main Function 69:6-69:10 +Stringer Interface 72:6-72:14 + String Method 73:2-73:8 +ABer Interface 76:6-76:10 + A Method 78:2-78:3 + B Method 77:2-77:3 +WithEmbeddeds Interface 81:6-81:19 + ABer Field 83:2-83:6 + Do Method 82:2-82:4 + Writer Field 84:5-84:11 +EmptyInterface Interface 87:6-87:20 +Dunk Function 89:6-89:10 +dunk Function 91:6-91:10 + diff --git a/internal/lsp/testdata/testy/testy.go b/gopls/internal/lsp/testdata/testy/testy.go similarity index 100% rename from internal/lsp/testdata/testy/testy.go rename to gopls/internal/lsp/testdata/testy/testy.go diff --git a/internal/lsp/testdata/testy/testy_test.go b/gopls/internal/lsp/testdata/testy/testy_test.go similarity index 73% rename from internal/lsp/testdata/testy/testy_test.go rename to gopls/internal/lsp/testdata/testy/testy_test.go index 4939f86b50b..a7e897840aa 100644 --- a/internal/lsp/testdata/testy/testy_test.go +++ b/gopls/internal/lsp/testdata/testy/testy_test.go @@ -3,12 +3,12 @@ package testy import ( "testing" - sig "golang.org/x/tools/internal/lsp/signature" - "golang.org/x/tools/internal/lsp/snippets" + sig "golang.org/lsptests/signature" + "golang.org/lsptests/snippets" ) func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *testing.T)", "", "func") - var x int //@mark(testyX, "x"),diag("x", "compiler", "x declared but not used", "error"),refs("x", testyX) + var x int //@mark(testyX, "x"),diag("x", "compiler", "x declared (and|but) not used", "error"),refs("x", testyX) a() //@mark(testyA, "a") } diff --git a/internal/lsp/testdata/testy/testy_test.go.golden b/gopls/internal/lsp/testdata/testy/testy_test.go.golden similarity index 100% rename from internal/lsp/testdata/testy/testy_test.go.golden rename to gopls/internal/lsp/testdata/testy/testy_test.go.golden diff --git a/internal/lsp/testdata/typdef/typdef.go b/gopls/internal/lsp/testdata/typdef/typdef.go similarity index 100% rename from internal/lsp/testdata/typdef/typdef.go rename to gopls/internal/lsp/testdata/typdef/typdef.go diff --git a/internal/lsp/testdata/typeassert/type_assert.go b/gopls/internal/lsp/testdata/typeassert/type_assert.go similarity index 100% rename from internal/lsp/testdata/typeassert/type_assert.go rename to gopls/internal/lsp/testdata/typeassert/type_assert.go diff --git a/internal/lsp/testdata/typeerrors/noresultvalues.go b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go similarity index 54% rename from internal/lsp/testdata/typeerrors/noresultvalues.go rename to gopls/internal/lsp/testdata/typeerrors/noresultvalues.go index 84234c4b93a..729e7bbccd4 100644 --- a/internal/lsp/testdata/typeerrors/noresultvalues.go +++ b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go @@ -1,5 +1,5 @@ package typeerrors -func x() { return nil } //@suggestedfix("nil", "quickfix") +func x() { return nil } //@suggestedfix("nil", "quickfix", "") -func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix") +func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix", "") diff --git a/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden new file mode 100644 index 00000000000..48409a0b7dd --- /dev/null +++ b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden @@ -0,0 +1,14 @@ +-- suggestedfix_noresultvalues_3_19 -- +package typeerrors + +func x() { return } //@suggestedfix("nil", "quickfix", "") + +func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix", "") + +-- suggestedfix_noresultvalues_5_19 -- +package typeerrors + +func x() { return nil } //@suggestedfix("nil", "quickfix", "") + +func y() { return } //@suggestedfix("nil", "quickfix", "") + diff --git a/internal/lsp/testdata/typemods/type_mods.go b/gopls/internal/lsp/testdata/typemods/type_mods.go similarity index 100% rename from internal/lsp/testdata/typemods/type_mods.go rename to gopls/internal/lsp/testdata/typemods/type_mods.go diff --git a/internal/lsp/testdata/typeparams/type_params.go b/gopls/internal/lsp/testdata/typeparams/type_params.go similarity index 94% rename from internal/lsp/testdata/typeparams/type_params.go rename to gopls/internal/lsp/testdata/typeparams/type_params.go index 715726b1a41..21fc7049f5b 100644 --- a/internal/lsp/testdata/typeparams/type_params.go +++ b/gopls/internal/lsp/testdata/typeparams/type_params.go @@ -42,7 +42,8 @@ func returnTP[A int | float64](a A) A { //@item(returnTP, "returnTP", "something } func _() { - var _ int = returnTP //@snippet(" //", returnTP, "returnTP[${1:}](${2:})", "returnTP[${1:A int|float64}](${2:a A})") + // disabled - see issue #54822 + var _ int = returnTP // snippet(" //", returnTP, "returnTP[${1:}](${2:})", "returnTP[${1:A int|float64}](${2:a A})") var aa int //@item(tpInt, "aa", "int", "var") var ab float64 //@item(tpFloat, "ab", "float64", "var") diff --git a/internal/lsp/testdata/types/types.go b/gopls/internal/lsp/testdata/types/types.go similarity index 100% rename from internal/lsp/testdata/types/types.go rename to gopls/internal/lsp/testdata/types/types.go diff --git a/gopls/internal/lsp/testdata/undeclared/var.go b/gopls/internal/lsp/testdata/undeclared/var.go new file mode 100644 index 00000000000..3fda582ce1f --- /dev/null +++ b/gopls/internal/lsp/testdata/undeclared/var.go @@ -0,0 +1,14 @@ +package undeclared + +func m() int { + z, _ := 1+y, 11 //@diag("y", "compiler", "(undeclared name|undefined): y", "error"),suggestedfix("y", "quickfix", "") + if 100 < 90 { + z = 1 + } else if 100 > n+2 { //@diag("n", "compiler", "(undeclared name|undefined): n", "error"),suggestedfix("n", "quickfix", "") + z = 4 + } + for i < 200 { //@diag("i", "compiler", "(undeclared name|undefined): i", "error"),suggestedfix("i", "quickfix", "") + } + r() //@diag("r", "compiler", "(undeclared name|undefined): r", "error") + return z +} diff --git a/gopls/internal/lsp/testdata/undeclared/var.go.golden b/gopls/internal/lsp/testdata/undeclared/var.go.golden new file mode 100644 index 00000000000..de5cbb42fbb --- /dev/null +++ b/gopls/internal/lsp/testdata/undeclared/var.go.golden @@ -0,0 +1,51 @@ +-- suggestedfix_var_10_6 -- +package undeclared + +func m() int { + z, _ := 1+y, 11 //@diag("y", "compiler", "(undeclared name|undefined): y", "error"),suggestedfix("y", "quickfix", "") + if 100 < 90 { + z = 1 + } else if 100 > n+2 { //@diag("n", "compiler", "(undeclared name|undefined): n", "error"),suggestedfix("n", "quickfix", "") + z = 4 + } + i := + for i < 200 { //@diag("i", "compiler", "(undeclared name|undefined): i", "error"),suggestedfix("i", "quickfix", "") + } + r() //@diag("r", "compiler", "(undeclared name|undefined): r", "error") + return z +} + +-- suggestedfix_var_4_12 -- +package undeclared + +func m() int { + y := + z, _ := 1+y, 11 //@diag("y", "compiler", "(undeclared name|undefined): y", "error"),suggestedfix("y", "quickfix", "") + if 100 < 90 { + z = 1 + } else if 100 > n+2 { //@diag("n", "compiler", "(undeclared name|undefined): n", "error"),suggestedfix("n", "quickfix", "") + z = 4 + } + for i < 200 { //@diag("i", "compiler", "(undeclared name|undefined): i", "error"),suggestedfix("i", "quickfix", "") + } + r() //@diag("r", "compiler", "(undeclared name|undefined): r", "error") + return z +} + +-- suggestedfix_var_7_18 -- +package undeclared + +func m() int { + z, _ := 1+y, 11 //@diag("y", "compiler", "(undeclared name|undefined): y", "error"),suggestedfix("y", "quickfix", "") + n := + if 100 < 90 { + z = 1 + } else if 100 > n+2 { //@diag("n", "compiler", "(undeclared name|undefined): n", "error"),suggestedfix("n", "quickfix", "") + z = 4 + } + for i < 200 { //@diag("i", "compiler", "(undeclared name|undefined): i", "error"),suggestedfix("i", "quickfix", "") + } + r() //@diag("r", "compiler", "(undeclared name|undefined): r", "error") + return z +} + diff --git a/internal/lsp/testdata/unimported/export_test.go b/gopls/internal/lsp/testdata/unimported/export_test.go similarity index 61% rename from internal/lsp/testdata/unimported/export_test.go rename to gopls/internal/lsp/testdata/unimported/export_test.go index 4f85700fa79..964d27d3b94 100644 --- a/internal/lsp/testdata/unimported/export_test.go +++ b/gopls/internal/lsp/testdata/unimported/export_test.go @@ -1,3 +1,3 @@ package unimported -var TestExport int //@item(testexport, "TestExport", "(from \"golang.org/x/tools/internal/lsp/unimported\")", "var") +var TestExport int //@item(testexport, "TestExport", "(from \"golang.org/lsptests/unimported\")", "var") diff --git a/internal/lsp/testdata/unimported/unimported.go.in b/gopls/internal/lsp/testdata/unimported/unimported.go.in similarity index 91% rename from internal/lsp/testdata/unimported/unimported.go.in rename to gopls/internal/lsp/testdata/unimported/unimported.go.in index c3c0243d901..4d1438d1bd8 100644 --- a/internal/lsp/testdata/unimported/unimported.go.in +++ b/gopls/internal/lsp/testdata/unimported/unimported.go.in @@ -14,7 +14,7 @@ func _() { /* ring.Ring */ //@item(ringring, "Ring", "(from \"container/ring\")", "var") -/* signature.Foo */ //@item(signaturefoo, "Foo", "func(a string, b int) (c bool) (from \"golang.org/x/tools/internal/lsp/signature\")", "func") +/* signature.Foo */ //@item(signaturefoo, "Foo", "func(a string, b int) (c bool) (from \"golang.org/lsptests/signature\")", "func") /* context.Background */ //@item(contextBackground, "Background", "func() context.Context (from \"context\")", "func") /* context.Background().Err */ //@item(contextBackgroundErr, "Background().Err", "func() error (from \"context\")", "method") diff --git a/internal/lsp/testdata/unimported/unimported_cand_type.go b/gopls/internal/lsp/testdata/unimported/unimported_cand_type.go similarity index 68% rename from internal/lsp/testdata/unimported/unimported_cand_type.go rename to gopls/internal/lsp/testdata/unimported/unimported_cand_type.go index 531aa2d180a..554c426a998 100644 --- a/internal/lsp/testdata/unimported/unimported_cand_type.go +++ b/gopls/internal/lsp/testdata/unimported/unimported_cand_type.go @@ -3,8 +3,8 @@ package unimported import ( _ "context" - "golang.org/x/tools/internal/lsp/baz" - _ "golang.org/x/tools/internal/lsp/signature" // provide type information for unimported completions in the other file + "golang.org/lsptests/baz" + _ "golang.org/lsptests/signature" // provide type information for unimported completions in the other file ) func _() { diff --git a/internal/lsp/testdata/unimported/x_test.go b/gopls/internal/lsp/testdata/unimported/x_test.go similarity index 100% rename from internal/lsp/testdata/unimported/x_test.go rename to gopls/internal/lsp/testdata/unimported/x_test.go diff --git a/internal/lsp/testdata/unresolved/unresolved.go.in b/gopls/internal/lsp/testdata/unresolved/unresolved.go.in similarity index 100% rename from internal/lsp/testdata/unresolved/unresolved.go.in rename to gopls/internal/lsp/testdata/unresolved/unresolved.go.in diff --git a/internal/lsp/testdata/unsafe/unsafe.go b/gopls/internal/lsp/testdata/unsafe/unsafe.go similarity index 100% rename from internal/lsp/testdata/unsafe/unsafe.go rename to gopls/internal/lsp/testdata/unsafe/unsafe.go diff --git a/internal/lsp/testdata/variadic/variadic.go.in b/gopls/internal/lsp/testdata/variadic/variadic.go.in similarity index 100% rename from internal/lsp/testdata/variadic/variadic.go.in rename to gopls/internal/lsp/testdata/variadic/variadic.go.in diff --git a/internal/lsp/testdata/variadic/variadic_intf.go b/gopls/internal/lsp/testdata/variadic/variadic_intf.go similarity index 100% rename from internal/lsp/testdata/variadic/variadic_intf.go rename to gopls/internal/lsp/testdata/variadic/variadic_intf.go diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a.go new file mode 100644 index 00000000000..4ae9997a03e --- /dev/null +++ b/gopls/internal/lsp/testdata/workspacesymbol/a/a.go @@ -0,0 +1,9 @@ +package a + +var RandomGopherVariableA = "a" + +const RandomGopherConstantA = "a" + +const ( + randomgopherinvariable = iota +) diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go new file mode 100644 index 00000000000..0d97c50d623 --- /dev/null +++ b/gopls/internal/lsp/testdata/workspacesymbol/a/a_test.go @@ -0,0 +1,3 @@ +package a + +var RandomGopherTestVariableA = "a" diff --git a/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go b/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go new file mode 100644 index 00000000000..747cd17eccd --- /dev/null +++ b/gopls/internal/lsp/testdata/workspacesymbol/a/a_x_test.go @@ -0,0 +1,3 @@ +package a_test + +var RandomGopherXTestVariableA = "a" diff --git a/gopls/internal/lsp/testdata/workspacesymbol/b/b.go b/gopls/internal/lsp/testdata/workspacesymbol/b/b.go new file mode 100644 index 00000000000..b2e2092eed6 --- /dev/null +++ b/gopls/internal/lsp/testdata/workspacesymbol/b/b.go @@ -0,0 +1,7 @@ +package b + +var RandomGopherVariableB = "b" + +type RandomGopherStructB struct { + Bar int +} diff --git a/internal/lsp/testdata/workspacesymbol/issue44806.go b/gopls/internal/lsp/testdata/workspacesymbol/issue44806.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/issue44806.go rename to gopls/internal/lsp/testdata/workspacesymbol/issue44806.go diff --git a/internal/lsp/testdata/workspacesymbol/main.go b/gopls/internal/lsp/testdata/workspacesymbol/main.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/main.go rename to gopls/internal/lsp/testdata/workspacesymbol/main.go diff --git a/internal/lsp/testdata/workspacesymbol/p/p.go b/gopls/internal/lsp/testdata/workspacesymbol/p/p.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/p/p.go rename to gopls/internal/lsp/testdata/workspacesymbol/p/p.go diff --git a/internal/lsp/testdata/workspacesymbol/query.go b/gopls/internal/lsp/testdata/workspacesymbol/query.go similarity index 100% rename from internal/lsp/testdata/workspacesymbol/query.go rename to gopls/internal/lsp/testdata/workspacesymbol/query.go diff --git a/internal/lsp/testdata/workspacesymbol/query.go.golden b/gopls/internal/lsp/testdata/workspacesymbol/query.go.golden similarity index 100% rename from internal/lsp/testdata/workspacesymbol/query.go.golden rename to gopls/internal/lsp/testdata/workspacesymbol/query.go.golden diff --git a/internal/lsp/tests/README.md b/gopls/internal/lsp/tests/README.md similarity index 91% rename from internal/lsp/tests/README.md rename to gopls/internal/lsp/tests/README.md index 2c18675f7e5..07df28815c1 100644 --- a/internal/lsp/tests/README.md +++ b/gopls/internal/lsp/tests/README.md @@ -11,7 +11,7 @@ file, like `internal/lsp/testdata/foo/bar.go.golden`. The former is the "input" and the latter is the expected output. Each input file contains annotations like -`//@suggestedfix("}", "refactor.rewrite")`. These annotations are interpreted by +`//@suggestedfix("}", "refactor.rewrite", "Fill anonymous struct")`. These annotations are interpreted by test runners to perform certain actions. The expected output after those actions is encoded in the golden file. @@ -26,7 +26,7 @@ in the golden file with a heading like, ``` The format of these headings vary: they are defined by the -[`Golden`](https://pkg.go.dev/golang.org/x/tools/internal/lsp/tests#Data.Golden) +[`Golden`](https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/tests#Data.Golden) function for each annotation. In the case above, the format is: annotation name, file name, annotation line location, annotation character location. diff --git a/gopls/internal/lsp/tests/compare/text.go b/gopls/internal/lsp/tests/compare/text.go new file mode 100644 index 00000000000..9521496feec --- /dev/null +++ b/gopls/internal/lsp/tests/compare/text.go @@ -0,0 +1,33 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package compare + +import ( + "golang.org/x/tools/internal/diff" +) + +// Text returns a formatted unified diff of the edits to go from want to +// got, returning "" if and only if want == got. +// +// This function is intended for use in testing, and panics if any error occurs +// while computing the diff. It is not sufficiently tested for production use. +func Text(want, got string) string { + if want == got { + return "" + } + + // Add newlines to avoid verbose newline messages ("No newline at end of file"). + unified := diff.Unified("want", "got", want+"\n", got+"\n") + + // Defensively assert that we get an actual diff, so that we guarantee the + // invariant that we return "" if and only if want == got. + // + // This is probably unnecessary, but convenient. + if unified == "" { + panic("empty diff for non-identical input") + } + + return unified +} diff --git a/gopls/internal/lsp/tests/compare/text_test.go b/gopls/internal/lsp/tests/compare/text_test.go new file mode 100644 index 00000000000..8f5af48bd11 --- /dev/null +++ b/gopls/internal/lsp/tests/compare/text_test.go @@ -0,0 +1,28 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package compare_test + +import ( + "testing" + + "golang.org/x/tools/gopls/internal/lsp/tests/compare" +) + +func TestText(t *testing.T) { + tests := []struct { + got, want, wantDiff string + }{ + {"", "", ""}, + {"equal", "equal", ""}, + {"a", "b", "--- want\n+++ got\n@@ -1 +1 @@\n-b\n+a\n"}, + {"a\nd\nc\n", "a\nb\nc\n", "--- want\n+++ got\n@@ -1,4 +1,4 @@\n a\n-b\n+d\n c\n \n"}, + } + + for _, test := range tests { + if gotDiff := compare.Text(test.want, test.got); gotDiff != test.wantDiff { + t.Errorf("compare.Text(%q, %q) =\n%q, want\n%q", test.want, test.got, gotDiff, test.wantDiff) + } + } +} diff --git a/gopls/internal/lsp/tests/markdown_go118.go b/gopls/internal/lsp/tests/markdown_go118.go new file mode 100644 index 00000000000..c8c5eef172b --- /dev/null +++ b/gopls/internal/lsp/tests/markdown_go118.go @@ -0,0 +1,63 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.19 +// +build !go1.19 + +package tests + +import ( + "regexp" + "strings" + "testing" + + "golang.org/x/tools/gopls/internal/lsp/tests/compare" +) + +// The markdown in the golden files matches the converter in comment.go, +// but for go1.19 and later the conversion is done using go/doc/comment. +// Compared to the newer version, the older version +// has extra escapes, and treats code blocks slightly differently. +func CheckSameMarkdown(t *testing.T, got, want string) { + t.Helper() + + got = normalizeMarkdown(got) + want = normalizeMarkdown(want) + + if diff := compare.Text(want, got); diff != "" { + t.Errorf("normalized markdown differs:\n%s", diff) + } +} + +// normalizeMarkdown normalizes whitespace and escaping of the input string, to +// eliminate differences between the Go 1.18 and Go 1.19 generated markdown for +// doc comments. Note that it does not normalize to either the 1.18 or 1.19 +// formatting: it simplifies both so that they may be compared. +// +// This function may need to be adjusted as we encounter more differences in +// the generated text. +func normalizeMarkdown(input string) string { + input = strings.TrimSpace(input) + + // For simplicity, eliminate blank lines. + input = regexp.MustCompile("\n+").ReplaceAllString(input, "\n") + + // Replace common escaped characters with their unescaped version. + // + // This list may not be exhaustive: it was just sufficient to make tests + // pass. + input = strings.NewReplacer( + `\\`, ``, + `\@`, `@`, + `\(`, `(`, + `\)`, `)`, + `\"`, `"`, + `\.`, `.`, + `\-`, `-`, + `\'`, `'`, + `\n\n\n`, `\n\n`, // Note that these are *escaped* newlines. + ).Replace(input) + + return input +} diff --git a/gopls/internal/lsp/tests/markdown_go119.go b/gopls/internal/lsp/tests/markdown_go119.go new file mode 100644 index 00000000000..e09ed62c4aa --- /dev/null +++ b/gopls/internal/lsp/tests/markdown_go119.go @@ -0,0 +1,26 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package tests + +import ( + "testing" + + "golang.org/x/tools/gopls/internal/lsp/tests/compare" +) + +// The markdown in the golden files matches the converter in comment.go, +// but for go1.19 and later the conversion is done using go/doc/comment. +// Compared to the newer version, the older version +// has extra escapes, and treats code blocks slightly differently. +func CheckSameMarkdown(t *testing.T, got, want string) { + t.Helper() + + if diff := compare.Text(want, got); diff != "" { + t.Errorf("normalized markdown differs:\n%s", diff) + } +} diff --git a/internal/lsp/tests/normalizer.go b/gopls/internal/lsp/tests/normalizer.go similarity index 100% rename from internal/lsp/tests/normalizer.go rename to gopls/internal/lsp/tests/normalizer.go diff --git a/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go similarity index 84% rename from internal/lsp/tests/tests.go rename to gopls/internal/lsp/tests/tests.go index 8265cf2e9b1..cab96e0e82c 100644 --- a/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -27,11 +27,12 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages/packagestest" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/source/completion" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/completion" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/txtar" @@ -41,7 +42,12 @@ const ( overlayFileSuffix = ".overlay" goldenFileSuffix = ".golden" inFileSuffix = ".in" - testModule = "golang.org/x/tools/internal/lsp" + + // The module path containing the testdata packages. + // + // Warning: the length of this module path matters, as we have bumped up + // against command-line limitations on windows (golang/go#54800). + testModule = "golang.org/lsptests" ) var summaryFile = "summary.txt" @@ -54,38 +60,39 @@ func init() { var UpdateGolden = flag.Bool("golden", false, "Update golden files") -type CallHierarchy map[span.Span]*CallHierarchyResult -type CodeLens map[span.URI][]protocol.CodeLens -type Diagnostics map[span.URI][]*source.Diagnostic -type CompletionItems map[token.Pos]*completion.CompletionItem -type Completions map[span.Span][]Completion -type CompletionSnippets map[span.Span][]CompletionSnippet -type UnimportedCompletions map[span.Span][]Completion -type DeepCompletions map[span.Span][]Completion -type FuzzyCompletions map[span.Span][]Completion -type CaseSensitiveCompletions map[span.Span][]Completion -type RankCompletions map[span.Span][]Completion -type FoldingRanges []span.Span -type Formats []span.Span -type Imports []span.Span -type SemanticTokens []span.Span -type SuggestedFixes map[span.Span][]string -type FunctionExtractions map[span.Span]span.Span -type MethodExtractions map[span.Span]span.Span -type Definitions map[span.Span]Definition -type Implementations map[span.Span][]span.Span -type Highlights map[span.Span][]span.Span -type References map[span.Span][]span.Span -type Renames map[span.Span]string -type PrepareRenames map[span.Span]*source.PrepareItem -type Symbols map[span.URI][]protocol.DocumentSymbol -type SymbolsChildren map[string][]protocol.DocumentSymbol -type SymbolInformation map[span.Span]protocol.SymbolInformation -type WorkspaceSymbols map[WorkspaceSymbolsTestType]map[span.URI][]string -type Signatures map[span.Span]*protocol.SignatureHelp -type Links map[span.URI][]Link -type AddImport map[span.URI]string -type Hovers map[span.Span]string +// These type names apparently avoid the need to repeat the +// type in the field name and the make() expression. +type CallHierarchy = map[span.Span]*CallHierarchyResult +type CodeLens = map[span.URI][]protocol.CodeLens +type Diagnostics = map[span.URI][]*source.Diagnostic +type CompletionItems = map[token.Pos]*completion.CompletionItem +type Completions = map[span.Span][]Completion +type CompletionSnippets = map[span.Span][]CompletionSnippet +type UnimportedCompletions = map[span.Span][]Completion +type DeepCompletions = map[span.Span][]Completion +type FuzzyCompletions = map[span.Span][]Completion +type CaseSensitiveCompletions = map[span.Span][]Completion +type RankCompletions = map[span.Span][]Completion +type FoldingRanges = []span.Span +type Formats = []span.Span +type Imports = []span.Span +type SemanticTokens = []span.Span +type SuggestedFixes = map[span.Span][]SuggestedFix +type FunctionExtractions = map[span.Span]span.Span +type MethodExtractions = map[span.Span]span.Span +type Definitions = map[span.Span]Definition +type Implementations = map[span.Span][]span.Span +type Highlights = map[span.Span][]span.Span +type References = map[span.Span][]span.Span +type Renames = map[span.Span]string +type PrepareRenames = map[span.Span]*source.PrepareItem +type Symbols = map[span.URI][]*symbol +type InlayHints = []span.Span +type WorkspaceSymbols = map[WorkspaceSymbolsTestType]map[span.URI][]string +type Signatures = map[span.Span]*protocol.SignatureHelp +type Links = map[span.URI][]Link +type AddImport = map[span.URI]string +type Hovers = map[span.Span]string type Data struct { Config packages.Config @@ -113,17 +120,15 @@ type Data struct { Highlights Highlights References References Renames Renames + InlayHints InlayHints PrepareRenames PrepareRenames Symbols Symbols - symbolsChildren SymbolsChildren - symbolInformation SymbolInformation WorkspaceSymbols WorkspaceSymbols Signatures Signatures Links Links AddImport AddImport Hovers Hovers - t testing.TB fragments map[string]string dir string golden map[string]*Golden @@ -135,6 +140,12 @@ type Data struct { mappers map[span.URI]*protocol.ColumnMapper } +// TODO(adonovan): there are multiple implementations of this (undocumented) +// interface, each of which must implement similar semantics. For example: +// - *runner in ../cmd/test/check.go +// - *runner in ../source/source_test.go +// - *runner in ../lsp_test.go +// Can we avoid this duplication? type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) CodeLens(*testing.T, span.URI, []protocol.CodeLens) @@ -150,12 +161,13 @@ type Tests interface { Format(*testing.T, span.Span) Import(*testing.T, span.Span) SemanticTokens(*testing.T, span.Span) - SuggestedFix(*testing.T, span.Span, []string, int) + SuggestedFix(*testing.T, span.Span, []SuggestedFix, int) FunctionExtraction(*testing.T, span.Span, span.Span) MethodExtraction(*testing.T, span.Span, span.Span) Definition(*testing.T, span.Span, Definition) Implementation(*testing.T, span.Span, []span.Span) Highlight(*testing.T, span.Span, []span.Span) + InlayHints(*testing.T, span.Span) References(*testing.T, span.Span, []span.Span) Rename(*testing.T, span.Span, string) PrepareRename(*testing.T, span.Span, *source.PrepareItem) @@ -229,6 +241,16 @@ type Link struct { NotePosition token.Position } +type SuggestedFix struct { + ActionKind, Title string +} + +// A symbol holds a DocumentSymbol along with its parent-child edge. +type symbol struct { + pSymbol protocol.DocumentSymbol + id, parentID string +} + type Golden struct { Filename string Archive *txtar.Archive @@ -262,6 +284,7 @@ func DefaultOptions(o *source.Options) { o.HierarchicalDocumentSymbolSupport = true o.ExperimentalWorkspaceModule = true o.SemanticTokens = true + o.InternalOptions.NewDiff = "both" } func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*testing.T, *Data)) { @@ -306,15 +329,12 @@ func load(t testing.TB, mode string, dir string) *Data { FunctionExtractions: make(FunctionExtractions), MethodExtractions: make(MethodExtractions), Symbols: make(Symbols), - symbolsChildren: make(SymbolsChildren), - symbolInformation: make(SymbolInformation), WorkspaceSymbols: make(WorkspaceSymbols), Signatures: make(Signatures), Links: make(Links), AddImport: make(AddImport), Hovers: make(Hovers), - t: t, dir: dir, fragments: map[string]string{}, golden: map[string]*Golden{}, @@ -466,6 +486,7 @@ func load(t testing.TB, mode string, dir string) *Data { "hoverdef": datum.collectHoverDefinitions, "hover": datum.collectHovers, "highlight": datum.collectHighlights, + "inlayHint": datum.collectInlayHints, "refs": datum.collectReferences, "rename": datum.collectRenames, "prepare": datum.collectPrepareRenames, @@ -481,12 +502,7 @@ func load(t testing.TB, mode string, dir string) *Data { }); err != nil { t.Fatal(err) } - for _, symbols := range datum.Symbols { - for i := range symbols { - children := datum.symbolsChildren[symbols[i].Name] - symbols[i].Children = children - } - } + // Collect names for the entries that require golden files. if err := datum.Exported.Expect(map[string]interface{}{ "godef": datum.collectDefinitionNames, @@ -782,6 +798,16 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) + t.Run("InlayHints", func(t *testing.T) { + t.Helper() + for _, src := range data.InlayHints { + t.Run(SpanName(src), func(t *testing.T) { + t.Helper() + tests.InlayHints(t, src) + }) + } + }) + t.Run("References", func(t *testing.T) { t.Helper() for src, itemList := range data.References { @@ -814,10 +840,44 @@ func Run(t *testing.T, tests Tests, data *Data) { t.Run("Symbols", func(t *testing.T) { t.Helper() - for uri, expectedSymbols := range data.Symbols { + for uri, allSymbols := range data.Symbols { + byParent := make(map[string][]*symbol) + for _, sym := range allSymbols { + if sym.parentID != "" { + byParent[sym.parentID] = append(byParent[sym.parentID], sym) + } + } + + // collectChildren does a depth-first traversal of the symbol tree, + // computing children of child nodes before returning to their parent. + // This is necessary as the Children field is slice of non-pointer types, + // and therefore we need to be careful to mutate children first before + // assigning them to their parent. + var collectChildren func(id string) []protocol.DocumentSymbol + collectChildren = func(id string) []protocol.DocumentSymbol { + children := byParent[id] + // delete from byParent before recursing, to ensure that + // collectChildren terminates even in the presence of cycles. + delete(byParent, id) + var result []protocol.DocumentSymbol + for _, child := range children { + child.pSymbol.Children = collectChildren(child.id) + result = append(result, child.pSymbol) + } + return result + } + + var topLevel []protocol.DocumentSymbol + for _, sym := range allSymbols { + if sym.parentID == "" { + sym.pSymbol.Children = collectChildren(sym.id) + topLevel = append(topLevel, sym.pSymbol) + } + } + t.Run(uriName(uri), func(t *testing.T) { t.Helper() - tests.Symbols(t, uri, expectedSymbols) + tests.Symbols(t, uri, topLevel) }) } }) @@ -970,6 +1030,7 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "DefinitionsCount = %v\n", definitionCount) fmt.Fprintf(buf, "TypeDefinitionsCount = %v\n", typeDefinitionCount) fmt.Fprintf(buf, "HighlightsCount = %v\n", len(data.Highlights)) + fmt.Fprintf(buf, "InlayHintsCount = %v\n", len(data.InlayHints)) fmt.Fprintf(buf, "ReferencesCount = %v\n", len(data.References)) fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames)) fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames)) @@ -979,12 +1040,15 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "LinksCount = %v\n", linksCount) fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations)) - want := string(data.Golden("summary", summaryFile, func() ([]byte, error) { + want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { return buf.Bytes(), nil })) got := buf.String() if want != got { - t.Errorf("test summary does not match:\n%s", Diff(t, want, got)) + // These counters change when assertions are added or removed. + // They act as an independent safety net to ensure that the + // tests didn't spuriously pass because they did no work. + t.Errorf("test summary does not match:\n%s\n(Run with -golden to update golden file; also, there may be one per Go version.)", compare.Text(want, got)) } } @@ -1002,19 +1066,19 @@ func (data *Data) Mapper(uri span.URI) (*protocol.ColumnMapper, error) { return data.mappers[uri], nil } -func (data *Data) Golden(tag string, target string, update func() ([]byte, error)) []byte { - data.t.Helper() +func (data *Data) Golden(t *testing.T, tag, target string, update func() ([]byte, error)) []byte { + t.Helper() fragment, found := data.fragments[target] if !found { if filepath.IsAbs(target) { - data.t.Fatalf("invalid golden file fragment %v", target) + t.Fatalf("invalid golden file fragment %v", target) } fragment = target } golden := data.golden[fragment] if golden == nil { if !*UpdateGolden { - data.t.Fatalf("could not find golden file %v: %v", fragment, tag) + t.Fatalf("could not find golden file %v: %v", fragment, tag) } golden = &Golden{ Filename: filepath.Join(data.dir, fragment+goldenFileSuffix), @@ -1040,14 +1104,14 @@ func (data *Data) Golden(tag string, target string, update func() ([]byte, error } contents, err := update() if err != nil { - data.t.Fatalf("could not update golden file %v: %v", fragment, err) + t.Fatalf("could not update golden file %v: %v", fragment, err) } file.Data = append(contents, '\n') // add trailing \n for txtar golden.Modified = true } if file == nil { - data.t.Fatalf("could not find golden contents %v: %v", fragment, tag) + t.Fatalf("could not find golden contents %v: %v", fragment, tag) } if len(file.Data) == 0 { return file.Data @@ -1056,19 +1120,8 @@ func (data *Data) Golden(tag string, target string, update func() ([]byte, error } func (data *Data) collectCodeLens(spn span.Span, title, cmd string) { - if _, ok := data.CodeLens[spn.URI()]; !ok { - data.CodeLens[spn.URI()] = []protocol.CodeLens{} - } - m, err := data.Mapper(spn.URI()) - if err != nil { - return - } - rng, err := m.Range(spn) - if err != nil { - return - } data.CodeLens[spn.URI()] = append(data.CodeLens[spn.URI()], protocol.CodeLens{ - Range: rng, + Range: data.mustRange(spn), Command: protocol.Command{ Title: title, Command: cmd, @@ -1076,18 +1129,7 @@ func (data *Data) collectCodeLens(spn span.Span, title, cmd string) { }) } -func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg, msgSeverity string) { - if _, ok := data.Diagnostics[spn.URI()]; !ok { - data.Diagnostics[spn.URI()] = []*source.Diagnostic{} - } - m, err := data.Mapper(spn.URI()) - if err != nil { - return - } - rng, err := m.Range(spn) - if err != nil { - return - } +func (data *Data) collectDiagnostics(spn span.Span, msgSource, msgPattern, msgSeverity string) { severity := protocol.SeverityError switch msgSeverity { case "error": @@ -1099,14 +1141,13 @@ func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg, msgSeverity case "information": severity = protocol.SeverityInformation } - // This is not the correct way to do this, but it seems excessive to do the full conversion here. - want := &source.Diagnostic{ - Range: rng, + + data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], &source.Diagnostic{ + Range: data.mustRange(spn), Severity: severity, Source: source.DiagnosticSource(msgSource), - Message: msg, - } - data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], want) + Message: msgPattern, + }) } func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) { @@ -1143,15 +1184,9 @@ func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []t } } -func (data *Data) collectCompletionItems(pos token.Pos, args []string) { - if len(args) < 3 { - loc := data.Exported.ExpectFileSet.Position(pos) - data.t.Fatalf("%s:%d: @item expects at least 3 args, got %d", - loc.Filename, loc.Line, len(args)) - } - label, detail, kind := args[0], args[1], args[2] +func (data *Data) collectCompletionItems(pos token.Pos, label, detail, kind string, args []string) { var documentation string - if len(args) == 4 { + if len(args) > 3 { documentation = args[3] } data.CompletionItems[pos] = &completion.CompletionItem{ @@ -1182,11 +1217,8 @@ func (data *Data) collectSemanticTokens(spn span.Span) { data.SemanticTokens = append(data.SemanticTokens, spn) } -func (data *Data) collectSuggestedFixes(spn span.Span, actionKind string) { - if _, ok := data.SuggestedFixes[spn]; !ok { - data.SuggestedFixes[spn] = []string{} - } - data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], actionKind) +func (data *Data) collectSuggestedFixes(spn span.Span, actionKind, fix string) { + data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], SuggestedFix{actionKind, fix}) } func (data *Data) collectFunctionExtractions(start span.Span, end span.Span) { @@ -1214,14 +1246,7 @@ func (data *Data) collectImplementations(src span.Span, targets []span.Span) { func (data *Data) collectIncomingCalls(src span.Span, calls []span.Span) { for _, call := range calls { - m, err := data.Mapper(call.URI()) - if err != nil { - data.t.Fatal(err) - } - rng, err := m.Range(call) - if err != nil { - data.t.Fatal(err) - } + rng := data.mustRange(call) // we're only comparing protocol.range if data.CallHierarchy[src] != nil { data.CallHierarchy[src].IncomingCalls = append(data.CallHierarchy[src].IncomingCalls, @@ -1244,19 +1269,11 @@ func (data *Data) collectOutgoingCalls(src span.Span, calls []span.Span) { data.CallHierarchy[src] = &CallHierarchyResult{} } for _, call := range calls { - m, err := data.Mapper(call.URI()) - if err != nil { - data.t.Fatal(err) - } - rng, err := m.Range(call) - if err != nil { - data.t.Fatal(err) - } // we're only comparing protocol.range data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls, protocol.CallHierarchyItem{ URI: protocol.DocumentURI(call.URI()), - Range: rng, + Range: data.mustRange(call), }) } } @@ -1292,6 +1309,10 @@ func (data *Data) collectHighlights(src span.Span, expected []span.Span) { data.Highlights[src] = append(data.Highlights[src], expected...) } +func (data *Data) collectInlayHints(src span.Span) { + data.InlayHints = append(data.InlayHints, src) +} + func (data *Data) collectReferences(src span.Span, expected []span.Span) { data.References[src] = expected } @@ -1300,57 +1321,38 @@ func (data *Data) collectRenames(src span.Span, newText string) { data.Renames[src] = newText } -func (data *Data) collectPrepareRenames(src span.Span, rng span.Range, placeholder string) { - m, err := data.Mapper(src.URI()) - if err != nil { - data.t.Fatal(err) - } - // Convert range to span and then to protocol.Range. - spn, err := rng.Span() - if err != nil { - data.t.Fatal(err) - } - prng, err := m.Range(spn) - if err != nil { - data.t.Fatal(err) - } +func (data *Data) collectPrepareRenames(src, spn span.Span, placeholder string) { data.PrepareRenames[src] = &source.PrepareItem{ - Range: prng, + Range: data.mustRange(spn), Text: placeholder, } } // collectSymbols is responsible for collecting @symbol annotations. -func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string, siName string) { +func (data *Data) collectSymbols(name string, selectionRng span.Span, kind, detail, id, parentID string) { + // We don't set 'Range' here as it is difficult (impossible?) to express + // multi-line ranges in the packagestest framework. + uri := selectionRng.URI() + data.Symbols[uri] = append(data.Symbols[uri], &symbol{ + pSymbol: protocol.DocumentSymbol{ + Name: name, + Kind: protocol.ParseSymbolKind(kind), + SelectionRange: data.mustRange(selectionRng), + Detail: detail, + }, + id: id, + parentID: parentID, + }) +} + +// mustRange converts spn into a protocol.Range, panicking on any error. +func (data *Data) mustRange(spn span.Span) protocol.Range { m, err := data.Mapper(spn.URI()) - if err != nil { - data.t.Fatal(err) - } rng, err := m.Range(spn) if err != nil { - data.t.Fatal(err) - } - sym := protocol.DocumentSymbol{ - Name: name, - Kind: protocol.ParseSymbolKind(kind), - SelectionRange: rng, - } - if parentName == "" { - data.Symbols[spn.URI()] = append(data.Symbols[spn.URI()], sym) - } else { - data.symbolsChildren[parentName] = append(data.symbolsChildren[parentName], sym) - } - - // Reuse @symbol in the workspace symbols tests. - si := protocol.SymbolInformation{ - Name: siName, - Kind: sym.Kind, - Location: protocol.Location{ - URI: protocol.URIFromSpanURI(spn.URI()), - Range: sym.SelectionRange, - }, + panic(fmt.Sprintf("converting span %s to range: %v", spn, err)) } - data.symbolInformation[spn] = si + return rng } func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(*expect.Note, string) { @@ -1401,6 +1403,8 @@ func uriName(uri span.URI) string { return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go")) } +// TODO(golang/go#54845): improve the formatting here to match standard +// line:column position formatting. func SpanName(spn span.Span) string { return fmt.Sprintf("%v_%v_%v", uriName(spn.URI()), spn.Start().Line(), spn.Start().Column()) } diff --git a/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go similarity index 77% rename from internal/lsp/tests/util.go rename to gopls/internal/lsp/tests/util.go index 11dda1f8edd..a90093a01cf 100644 --- a/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -9,18 +9,19 @@ import ( "context" "fmt" "go/token" + "path" "path/filepath" + "regexp" "sort" "strconv" "strings" "testing" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/myers" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/source/completion" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/source/completion" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/span" ) // DiffLinks takes the links we got and checks if they are located within the source or a Note. @@ -65,75 +66,69 @@ func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []proto return "" } -// DiffSymbols prints the diff between expected and actual symbols test results. -func DiffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string { - sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name }) - sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name }) - if len(got) != len(want) { - return summarizeSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want)) - } - for i, w := range want { - g := got[i] - if w.Name != g.Name { - return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name) - } - if w.Kind != g.Kind { - return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind) - } - if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 { - return summarizeSymbols(i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange) +// CompareDiagnostics reports testing errors to t when the diagnostic set got +// does not match want. If the sole expectation has source "no_diagnostics", +// the test expects that no diagnostics were received for the given document. +func CompareDiagnostics(t *testing.T, uri span.URI, want, got []*source.Diagnostic) { + t.Helper() + fileName := path.Base(string(uri)) + + // A special case to test that there are no diagnostics for a file. + if len(want) == 1 && want[0].Source == "no_diagnostics" { + want = nil + } + + // Build a helper function to match an actual diagnostic to an overlapping + // expected diagnostic (if any). + unmatched := make([]*source.Diagnostic, len(want)) + copy(unmatched, want) + source.SortDiagnostics(unmatched) + match := func(g *source.Diagnostic) *source.Diagnostic { + // Find the last expected diagnostic d for which start(d) < end(g), and + // check to see if it overlaps. + i := sort.Search(len(unmatched), func(i int) bool { + d := unmatched[i] + // See rangeOverlaps: if a range is a single point, we consider End to be + // included in the range... + if g.Range.Start == g.Range.End { + return protocol.ComparePosition(d.Range.Start, g.Range.End) > 0 + } + // ...otherwise the end position of a range is not included. + return protocol.ComparePosition(d.Range.Start, g.Range.End) >= 0 + }) + if i == 0 { + return nil } - if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" { - return fmt.Sprintf("children of %s: %s", w.Name, msg) + w := unmatched[i-1] + if rangeOverlaps(w.Range, g.Range) { + unmatched = append(unmatched[:i-1], unmatched[i:]...) + return w } + return nil } - return "" -} -func summarizeSymbols(i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string { - msg := &bytes.Buffer{} - fmt.Fprint(msg, "document symbols failed") - if i >= 0 { - fmt.Fprintf(msg, " at %d", i) - } - fmt.Fprint(msg, " because of ") - fmt.Fprintf(msg, reason, args...) - fmt.Fprint(msg, ":\nexpected:\n") - for _, s := range want { - fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange) - } - fmt.Fprintf(msg, "got:\n") - for _, s := range got { - fmt.Fprintf(msg, " %v %v %v\n", s.Name, s.Kind, s.SelectionRange) - } - return msg.String() -} - -// DiffDiagnostics prints the diff between expected and actual diagnostics test -// results. -func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string { - source.SortDiagnostics(want) - source.SortDiagnostics(got) - - if len(got) != len(want) { - return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want)) - } - for i, w := range want { - g := got[i] - if w.Message != g.Message { - return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message) + for _, g := range got { + w := match(g) + if w == nil { + t.Errorf("%s:%s: unexpected diagnostic %q", fileName, g.Range, g.Message) + continue + } + if match, err := regexp.MatchString(w.Message, g.Message); err != nil { + t.Errorf("%s:%s: invalid regular expression %q: %v", fileName, w.Range.Start, w.Message, err) + } else if !match { + t.Errorf("%s:%s: got Message %q, want match for pattern %q", fileName, g.Range.Start, g.Message, w.Message) } if w.Severity != g.Severity { - return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity) + t.Errorf("%s:%s: got Severity %v, want %v", fileName, g.Range.Start, g.Severity, w.Severity) } if w.Source != g.Source { - return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source) - } - if !rangeOverlaps(g.Range, w.Range) { - return summarizeDiagnostics(i, uri, want, got, "range %v does not overlap %v", g.Range, w.Range) + t.Errorf("%s:%s: got Source %v, want %v", fileName, g.Range.Start, g.Source, w.Source) } } - return "" + + for _, w := range unmatched { + t.Errorf("%s:%s: unmatched diagnostic pattern %q", fileName, w.Range, w.Message) + } } // rangeOverlaps reports whether r1 and r2 overlap. @@ -157,25 +152,6 @@ func inRange(p protocol.Position, r protocol.Range) bool { return false } -func summarizeDiagnostics(i int, uri span.URI, want, got []*source.Diagnostic, reason string, args ...interface{}) string { - msg := &bytes.Buffer{} - fmt.Fprint(msg, "diagnostics failed") - if i >= 0 { - fmt.Fprintf(msg, " at %d", i) - } - fmt.Fprint(msg, " because of ") - fmt.Fprintf(msg, reason, args...) - fmt.Fprint(msg, ":\nexpected:\n") - for _, d := range want { - fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message) - } - fmt.Fprintf(msg, "got:\n") - for _, d := range got { - fmt.Fprintf(msg, " %s:%v: %s\n", uri, d.Range, d.Message) - } - return msg.String() -} - func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string { sortCodeLens(want) sortCodeLens(got) @@ -237,28 +213,23 @@ func summarizeCodeLens(i int, uri span.URI, want, got []protocol.CodeLens, reaso return msg.String() } -func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, error) { +func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) string { decorate := func(f string, args ...interface{}) string { return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...)) } if len(got.Signatures) != 1 { - return decorate("wanted 1 signature, got %d", len(got.Signatures)), nil + return decorate("wanted 1 signature, got %d", len(got.Signatures)) } if got.ActiveSignature != 0 { - return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)), nil + return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)) } if want.ActiveParameter != got.ActiveParameter { - return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)), nil + return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)) } g := got.Signatures[0] w := want.Signatures[0] - if NormalizeAny(w.Label) != NormalizeAny(g.Label) { - wLabel := w.Label + "\n" - d, err := myers.ComputeEdits("", wLabel, g.Label+"\n") - if err != nil { - return "", err - } - return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d)), err + if diff := compare.Text(NormalizeAny(w.Label), NormalizeAny(g.Label)); diff != "" { + return decorate("mismatched labels:\n%s", diff) } var paramParts []string for _, p := range g.Parameters { @@ -266,9 +237,9 @@ func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, e } paramsStr := strings.Join(paramParts, ", ") if !strings.Contains(g.Label, paramsStr) { - return decorate("expected signature %q to contain params %q", g.Label, paramsStr), nil + return decorate("expected signature %q to contain params %q", g.Label, paramsStr) } - return "", nil + return "" } // NormalizeAny replaces occurrences of interface{} in input with any. @@ -512,6 +483,15 @@ func EnableAllAnalyzers(view source.View, opts *source.Options) { } } +func EnableAllInlayHints(view source.View, opts *source.Options) { + if opts.Hints == nil { + opts.Hints = make(map[string]bool) + } + for name := range source.AllInlayHints { + opts.Hints[name] = true + } +} + func WorkspaceSymbolsString(ctx context.Context, data *Data, queryURI span.URI, symbols []protocol.SymbolInformation) (string, error) { queryDir := filepath.Dir(queryURI.Filename()) var filtered []string @@ -546,20 +526,6 @@ func WorkspaceSymbolsTestTypeToMatcher(typ WorkspaceSymbolsTestType) source.Symb } } -func Diff(t *testing.T, want, got string) string { - if want == got { - return "" - } - // Add newlines to avoid newline messages in diff. - want += "\n" - got += "\n" - d, err := myers.ComputeEdits("", want, got) - if err != nil { - t.Fatal(err) - } - return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d)) -} - // StripSubscripts removes type parameter id subscripts. // // TODO(rfindley): remove this function once subscripts are removed from the diff --git a/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go similarity index 91% rename from internal/lsp/text_synchronization.go rename to gopls/internal/lsp/text_synchronization.go index 3276a47bf99..ab765b60dd3 100644 --- a/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -12,11 +12,11 @@ import ( "path/filepath" "time" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" ) @@ -40,6 +40,9 @@ const ( // FromDidClose is a file modification caused by closing a file. FromDidClose + // TODO: add FromDidChangeConfiguration, once configuration changes cause a + // new snapshot to be created. + // FromRegenerateCgo refers to file modifications caused by regenerating // the cgo sources for the workspace. FromRegenerateCgo @@ -286,21 +289,38 @@ func (s *Server) processModifications(ctx context.Context, modifications []sourc return errors.New("server is shut down") } s.stateMu.Unlock() + // If the set of changes included directories, expand those directories // to their files. modifications = s.session.ExpandModificationsToDirectories(ctx, modifications) - snapshots, releases, err := s.session.DidModifyFiles(ctx, modifications) + // Build a lookup map for file modifications, so that we can later join + // with the snapshot file associations. + modMap := make(map[span.URI]source.FileModification) + for _, mod := range modifications { + modMap[mod.URI] = mod + } + + snapshots, release, err := s.session.DidModifyFiles(ctx, modifications) if err != nil { close(diagnoseDone) return err } + // golang/go#50267: diagnostics should be re-sent after an open or close. For + // some clients, it may be helpful to re-send after each change. + for snapshot, uris := range snapshots { + for _, uri := range uris { + mod := modMap[uri] + if snapshot.View().Options().ChattyDiagnostics || mod.Action == source.Open || mod.Action == source.Close { + s.mustPublishDiagnostics(uri) + } + } + } + go func() { s.diagnoseSnapshots(snapshots, onDisk) - for _, release := range releases { - release() - } + release() close(diagnoseDone) }() @@ -339,6 +359,9 @@ func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, chan return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) } for _, change := range changes { + // TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t. + // out-of-order or overlapping changes---and much more efficient. + // Make sure to update column mapper along with the content. m := protocol.NewColumnMapper(uri, content) if change.Range == nil { @@ -348,9 +371,6 @@ func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, chan if err != nil { return nil, err } - if !spn.HasOffset() { - return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) - } start, end := spn.Start().Offset(), spn.End().Offset() if end < start { return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) diff --git a/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go similarity index 97% rename from internal/lsp/work/completion.go rename to gopls/internal/lsp/work/completion.go index c7227bc268b..623d2ce8bda 100644 --- a/internal/lsp/work/completion.go +++ b/gopls/internal/lsp/work/completion.go @@ -15,8 +15,8 @@ import ( "strings" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) { diff --git a/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go similarity index 87% rename from internal/lsp/work/diagnostics.go rename to gopls/internal/lsp/work/diagnostics.go index e583e60fd75..0d0f4eb18d9 100644 --- a/internal/lsp/work/diagnostics.go +++ b/gopls/internal/lsp/work/diagnostics.go @@ -11,11 +11,11 @@ import ( "path/filepath" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/event/tag" ) func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { @@ -59,7 +59,7 @@ func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source // Add diagnostic if a directory does not contain a module. var diagnostics []*source.Diagnostic for _, use := range pw.File.Use { - rng, err := source.LineToRange(pw.Mapper, fh.URI(), use.Syntax.Start, use.Syntax.End) + rng, err := pw.Mapper.OffsetRange(use.Syntax.Start.Byte, use.Syntax.End.Byte) if err != nil { return nil, err } @@ -73,7 +73,7 @@ func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source URI: fh.URI(), Range: rng, Severity: protocol.SeverityError, - Source: source.UnknownError, // Do we need a new source for this? + Source: source.WorkFileError, Message: fmt.Sprintf("directory %v does not contain a module", use.Path), }) } diff --git a/internal/lsp/work/format.go b/gopls/internal/lsp/work/format.go similarity index 68% rename from internal/lsp/work/format.go rename to gopls/internal/lsp/work/format.go index 35b804a73b5..e852eb4d27e 100644 --- a/internal/lsp/work/format.go +++ b/gopls/internal/lsp/work/format.go @@ -8,9 +8,9 @@ import ( "context" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" ) func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) { @@ -23,9 +23,6 @@ func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) } formatted := modfile.Format(pw.File.Syntax) // Calculate the edits to be made due to the change. - diff, err := snapshot.View().Options().ComputeEdits(fh.URI(), string(pw.Mapper.Content), string(formatted)) - if err != nil { - return nil, err - } - return source.ToProtocolEdits(pw.Mapper, diff) + diffs := snapshot.View().Options().ComputeEdits(string(pw.Mapper.Content), string(formatted)) + return source.ToProtocolEdits(pw.Mapper, diffs) } diff --git a/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go similarity index 93% rename from internal/lsp/work/hover.go rename to gopls/internal/lsp/work/hover.go index 8f7822d5b4b..641028b16e6 100644 --- a/internal/lsp/work/hover.go +++ b/gopls/internal/lsp/work/hover.go @@ -11,9 +11,9 @@ import ( "go/token" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" ) func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { @@ -56,7 +56,7 @@ func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, mod := pm.File.Module.Mod // Get the range to highlight for the hover. - rng, err := source.ByteOffsetsToRange(pw.Mapper, fh.URI(), pathStart, pathEnd) + rng, err := pw.Mapper.OffsetRange(pathStart, pathEnd) if err != nil { return nil, err } diff --git a/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go similarity index 87% rename from internal/lsp/workspace.go rename to gopls/internal/lsp/workspace.go index a1f837e2309..4f9948ec9d3 100644 --- a/internal/lsp/workspace.go +++ b/gopls/internal/lsp/workspace.go @@ -8,9 +8,9 @@ import ( "context" "fmt" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) func (s *Server) didChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error { @@ -26,16 +26,18 @@ func (s *Server) didChangeWorkspaceFolders(ctx context.Context, params *protocol return s.addFolders(ctx, event.Added) } +// addView returns a Snapshot and a release function that must be +// called when it is no longer needed. func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source.Snapshot, func(), error) { s.stateMu.Lock() state := s.state s.stateMu.Unlock() if state < serverInitialized { - return nil, func() {}, fmt.Errorf("addView called before server initialized") + return nil, nil, fmt.Errorf("addView called before server initialized") } options := s.session.Options().Clone() if err := s.fetchConfig(ctx, name, uri, options); err != nil { - return nil, func() {}, err + return nil, nil, err } _, snapshot, release, err := s.session.NewView(ctx, name, uri, options) return snapshot, release, err @@ -67,6 +69,9 @@ func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChan }() } + // An options change may have affected the detected Go version. + s.checkViewGoVersions() + registration := semanticTokenRegistration(options.SemanticTypes, options.SemanticMods) // Update any session-specific registrations or unregistrations. if !semanticTokensRegistered && options.SemanticTokens { diff --git a/internal/lsp/workspace_symbol.go b/gopls/internal/lsp/workspace_symbol.go similarity index 86% rename from internal/lsp/workspace_symbol.go rename to gopls/internal/lsp/workspace_symbol.go index 20c5763ab73..9101a3e7d11 100644 --- a/internal/lsp/workspace_symbol.go +++ b/gopls/internal/lsp/workspace_symbol.go @@ -8,8 +8,8 @@ import ( "context" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/source" ) func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { diff --git a/gopls/internal/migrate.sh b/gopls/internal/migrate.sh new file mode 100755 index 00000000000..6f2bebc6ad6 --- /dev/null +++ b/gopls/internal/migrate.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# +# Copyright 2022 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +# +# Migrates the internal/lsp directory to gopls/internal/lsp. Run this script +# from the root of x/tools to migrate in-progress CLs. +# +# See golang/go#54509 for more details. This script may be deleted once a +# reasonable amount of time has passed such that all active in-progress CLs +# have been rebased. + +set -eu + +# A portable -i flag. Darwin requires two parameters. +# See https://stackoverflow.com/questions/5694228/sed-in-place-flag-that-works-both-on-mac-bsd-and-linux +# for more details. +sedi=(-i) +case "$(uname)" in + Darwin*) sedi=(-i "") +esac + +# mvpath moves the directory at the relative path $1 to the relative path $2, +# moving files and rewriting import paths. +# +# It uses heuristics to identify import path literals, and therefore may be +# imprecise. +function mvpath() { + # If the source also doesn't exist, it may have already been moved. + # Skip so that this script is idempotent. + if [[ ! -d $1 ]]; then + echo "WARNING: skipping nonexistent source directory $1" + return 0 + fi + + # git can sometimes leave behind empty directories, which can change the + # behavior of the mv command below. + if [[ -d $2 ]] || [[ -f $2 ]]; then + echo "ERROR: destination $2 already exists" + exit 1 + fi + + mv $1 $2 + + local old="golang.org/x/tools/$1" + local new="golang.org/x/tools/$2" + + # Replace instances of the old import path with the new. This is imprecise, + # but we are a bit careful to avoid replacing golang.org/x/tools/foox with + # golang.org/x/tools/barx when moving foo->bar: the occurrence of the import + # path must be followed by whitespace, /, or a closing " or `. + local replace="s:${old}\([[:space:]/\"\`]\):${new}\1:g" + find . -type f \( \ + -name ".git" -prune -o \ + -name "*.go" -o \ + -name "*.in" -o \ + -name "*.golden" -o \ + -name "*.hlp" -o \ + -name "*.md" \) \ + -exec sed "${sedi[@]}" -e $replace {} \; +} + +mvpath internal/lsp/diff internal/diff +mvpath internal/lsp/fuzzy internal/fuzzy +mvpath internal/lsp/debug/tag internal/event/tag +mvpath internal/lsp/bug internal/bug +mvpath internal/lsp gopls/internal/lsp diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index 5e4eb5fc23a..98d783c67c9 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -5,190 +5,273 @@ package bench import ( + "context" "flag" "fmt" + "io/ioutil" + "log" "os" - "runtime/pprof" + "os/exec" + "path/filepath" + "sync" "testing" + "time" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/fake" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/fakenet" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) +// This package implements benchmarks that share a common editor session. +// +// It is a work-in-progress. +// +// Remaining TODO(rfindley): +// - add detailed documentation for how to write a benchmark, as a package doc +// - add benchmarks for more features +// - eliminate flags, and just run benchmarks on with a predefined set of +// arguments + func TestMain(m *testing.M) { bug.PanicOnBugs = true - Main(m, hooks.Options) + event.SetExporter(nil) // don't log to stderr + code := doMain(m) + os.Exit(code) } -func benchmarkOptions(dir string) []RunOption { - return []RunOption{ - // Run in an existing directory, since we're trying to simulate known cases - // that cause gopls memory problems. - InExistingDir(dir), - // Skip logs as they buffer up memory unnaturally. - SkipLogs(), - // The Debug server only makes sense if running in singleton mode. - Modes(Singleton), - // Remove the default timeout. Individual tests should control their - // own graceful termination. - NoDefaultTimeout(), - - // Use the actual proxy, since we want our builds to succeed. - GOPROXY("https://proxy.golang.org"), - } +func doMain(m *testing.M) (code int) { + defer func() { + if editor != nil { + if err := editor.Close(context.Background()); err != nil { + fmt.Fprintf(os.Stderr, "closing editor: %v", err) + if code == 0 { + code = 1 + } + } + } + if tempDir != "" { + if err := os.RemoveAll(tempDir); err != nil { + fmt.Fprintf(os.Stderr, "cleaning temp dir: %v", err) + if code == 0 { + code = 1 + } + } + } + }() + return m.Run() } -func printBenchmarkResults(result testing.BenchmarkResult) { - fmt.Printf("BenchmarkStatistics\t%s\t%s\n", result.String(), result.MemString()) -} +var ( + workdir = flag.String("workdir", "", "if set, working directory to use for benchmarks; overrides -repo and -commit") + repo = flag.String("repo", "https://go.googlesource.com/tools", "if set (and -workdir is unset), run benchmarks in this repo") + file = flag.String("file", "go/ast/astutil/util.go", "active file, for benchmarks that operate on a file") + commitish = flag.String("commit", "gopls/v0.9.0", "if set (and -workdir is unset), run benchmarks at this commit") + + goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_commit") + goplsCommit = flag.String("gopls_commit", "", "if set, install and use gopls at this commit for testing; incompatible with -gopls_path") + + // If non-empty, tempDir is a temporary working dir that was created by this + // test suite. + // + // The sync.Once variables guard various modifications of the temp directory. + makeTempDirOnce sync.Once + checkoutRepoOnce sync.Once + installGoplsOnce sync.Once + tempDir string + + setupEditorOnce sync.Once + sandbox *fake.Sandbox + editor *fake.Editor + awaiter *Awaiter +) -var iwlOptions struct { - workdir string +// getTempDir returns the temporary directory to use for benchmark files, +// creating it if necessary. +func getTempDir() string { + makeTempDirOnce.Do(func() { + var err error + tempDir, err = ioutil.TempDir("", "gopls-bench") + if err != nil { + log.Fatal(err) + } + }) + return tempDir } -func init() { - flag.StringVar(&iwlOptions.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory") +// benchmarkDir returns the directory to use for benchmarks. +// +// If -workdir is set, just use that directory. Otherwise, check out a shallow +// copy of -repo at the given -commit, and clean up when the test suite exits. +func benchmarkDir() string { + if *workdir != "" { + return *workdir + } + if *repo == "" { + log.Fatal("-repo must be provided if -workdir is unset") + } + if *commitish == "" { + log.Fatal("-commit must be provided if -workdir is unset") + } + + dir := filepath.Join(getTempDir(), "repo") + checkoutRepoOnce.Do(func() { + log.Printf("creating working dir: checking out %s@%s to %s\n", *repo, *commitish, dir) + if err := shallowClone(dir, *repo, *commitish); err != nil { + log.Fatal(err) + } + }) + return dir } -func TestBenchmarkIWL(t *testing.T) { - if iwlOptions.workdir == "" { - t.Skip("-iwl_workdir not configured") +// shallowClone performs a shallow clone of repo into dir at the given +// 'commitish' ref (any commit reference understood by git). +// +// The directory dir must not already exist. +func shallowClone(dir, repo, commitish string) error { + if err := os.Mkdir(dir, 0750); err != nil { + return fmt.Errorf("creating dir for %s: %v", repo, err) } - opts := stressTestOptions(iwlOptions.workdir) - // Don't skip hooks, so that we can wait for IWL. - opts = append(opts, SkipHooks(false)) + // Set a timeout for git fetch. If this proves flaky, it can be removed. + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + // Use a shallow fetch to download just the relevant commit. + shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", repo, commitish) + initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) + initCmd.Dir = dir + if output, err := initCmd.CombinedOutput(); err != nil { + return fmt.Errorf("checking out %s: %v\n%s", repo, err, output) + } + return nil +} + +// benchmarkEnv returns a shared benchmark environment +func benchmarkEnv(tb testing.TB) *Env { + setupEditorOnce.Do(func() { + dir := benchmarkDir() - results := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {}) + var err error + sandbox, editor, awaiter, err = connectEditor(dir, fake.EditorConfig{}) + if err != nil { + log.Fatalf("connecting editor: %v", err) + } + + if err := awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil { + panic(err) } }) - printBenchmarkResults(results) + return &Env{ + T: tb, + Ctx: context.Background(), + Editor: editor, + Sandbox: sandbox, + Awaiter: awaiter, + } } -var symbolOptions struct { - workdir, query, matcher, style string - printResults bool -} +// connectEditor connects a fake editor session in the given dir, using the +// given editor config. +func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.Editor, *Awaiter, error) { + s, err := fake.NewSandbox(&fake.SandboxConfig{ + Workdir: dir, + GOPROXY: "https://proxy.golang.org", + }) + if err != nil { + return nil, nil, nil, err + } -func init() { - flag.StringVar(&symbolOptions.workdir, "symbol_workdir", "", "if set, run symbol benchmark in this directory") - flag.StringVar(&symbolOptions.query, "symbol_query", "test", "symbol query to use in benchmark") - flag.StringVar(&symbolOptions.matcher, "symbol_matcher", "", "symbol matcher to use in benchmark") - flag.StringVar(&symbolOptions.style, "symbol_style", "", "symbol style to use in benchmark") - flag.BoolVar(&symbolOptions.printResults, "symbol_print_results", false, "whether to print symbol query results") + a := NewAwaiter(s.Workdir) + ts := getServer() + e, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks()) + if err != nil { + return nil, nil, nil, err + } + return s, e, a, nil } -func TestBenchmarkSymbols(t *testing.T) { - if symbolOptions.workdir == "" { - t.Skip("-symbol_workdir not configured") +// getServer returns a server connector that either starts a new in-process +// server, or starts a separate gopls process. +func getServer() servertest.Connector { + if *goplsPath != "" && *goplsCommit != "" { + panic("can't set both -gopls_path and -gopls_commit") } - - opts := benchmarkOptions(symbolOptions.workdir) - conf := EditorConfig{} - if symbolOptions.matcher != "" { - conf.SymbolMatcher = &symbolOptions.matcher + if *goplsPath != "" { + return &SidecarServer{*goplsPath} } - if symbolOptions.style != "" { - conf.SymbolStyle = &symbolOptions.style + if *goplsCommit != "" { + path := getInstalledGopls() + return &SidecarServer{path} } - opts = append(opts, conf) - - WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { - // We can't Await in this test, since we have disabled hooks. Instead, run - // one symbol request to completion to ensure all necessary cache entries - // are populated. - symbols, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{ - Query: symbolOptions.query, - }) - if err != nil { - t.Fatal(err) + server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false) + return servertest.NewPipeServer(server, jsonrpc2.NewRawStream) +} + +// getInstalledGopls builds gopls at the given -gopls_commit, returning the +// path to the gopls binary. +func getInstalledGopls() string { + if *goplsCommit == "" { + panic("must provide -gopls_commit") + } + toolsDir := filepath.Join(getTempDir(), "tools") + goplsPath := filepath.Join(toolsDir, "gopls", "gopls") + + installGoplsOnce.Do(func() { + log.Printf("installing gopls: checking out x/tools@%s\n", *goplsCommit) + if err := shallowClone(toolsDir, "https://go.googlesource.com/tools", *goplsCommit); err != nil { + log.Fatal(err) } - if symbolOptions.printResults { - fmt.Println("Results:") - for i := 0; i < len(symbols); i++ { - fmt.Printf("\t%d. %s (%s)\n", i, symbols[i].Name, symbols[i].ContainerName) - } + log.Println("installing gopls: building...") + bld := exec.Command("go", "build", ".") + bld.Dir = filepath.Join(getTempDir(), "tools", "gopls") + if output, err := bld.CombinedOutput(); err != nil { + log.Fatalf("building gopls: %v\n%s", err, output) } - results := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - if _, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{ - Query: symbolOptions.query, - }); err != nil { - t.Fatal(err) - } - } - }) - printBenchmarkResults(results) + // Confirm that the resulting path now exists. + if _, err := os.Stat(goplsPath); err != nil { + log.Fatalf("os.Stat(%s): %v", goplsPath, err) + } }) + return goplsPath } -var ( - benchDir = flag.String("didchange_dir", "", "If set, run benchmarks in this dir. Must also set didchange_file.") - benchFile = flag.String("didchange_file", "", "The file to modify") - benchProfile = flag.String("didchange_cpuprof", "", "file to write cpu profiling data to") -) +// A SidecarServer starts (and connects to) a separate gopls process at the +// given path. +type SidecarServer struct { + goplsPath string +} -// TestBenchmarkDidChange benchmarks modifications of a single file by making -// synthetic modifications in a comment. It controls pacing by waiting for the -// server to actually start processing the didChange notification before -// proceeding. Notably it does not wait for diagnostics to complete. -// -// Run it by passing -didchange_dir and -didchange_file, where -didchange_dir -// is the path to a workspace root, and -didchange_file is the -// workspace-relative path to a file to modify. e.g.: -// -// go test -run=TestBenchmarkDidChange \ -// -didchange_dir=path/to/kubernetes \ -// -didchange_file=pkg/util/hash/hash.go -func TestBenchmarkDidChange(t *testing.T) { - if *benchDir == "" { - t.Skip("-didchange_dir is not set") +// Connect creates new io.Pipes and binds them to the underlying StreamServer. +func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn { + cmd := exec.CommandContext(ctx, s.goplsPath, "serve") + + stdin, err := cmd.StdinPipe() + if err != nil { + log.Fatal(err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Fatal(err) } - if *benchFile == "" { - t.Fatal("-didchange_file must be set if -didchange_dir is set") + cmd.Stderr = os.Stdout + if err := cmd.Start(); err != nil { + log.Fatalf("starting gopls: %v", err) } - opts := benchmarkOptions(*benchDir) - WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { - env.OpenFile(*benchFile) - env.Await(env.DoneWithOpen()) - // Insert the text we'll be modifying at the top of the file. - env.EditBuffer(*benchFile, fake.Edit{Text: "// __REGTEST_PLACEHOLDER_0__\n"}) - - // Run the profiler after the initial load, - // across all benchmark iterations. - if *benchProfile != "" { - profile, err := os.Create(*benchProfile) - if err != nil { - t.Fatal(err) - } - defer profile.Close() - if err := pprof.StartCPUProfile(profile); err != nil { - t.Fatal(err) - } - defer pprof.StopCPUProfile() - } + go cmd.Wait() // to free resources; error is ignored - result := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - env.EditBuffer(*benchFile, fake.Edit{ - Start: fake.Pos{Line: 0, Column: 0}, - End: fake.Pos{Line: 1, Column: 0}, - // Increment - Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), - }) - env.Await(StartedChange(uint64(i + 1))) - } - }) - printBenchmarkResults(result) - }) + clientStream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", stdout, stdin)) + clientConn := jsonrpc2.NewConn(clientStream) + return clientConn } diff --git a/gopls/internal/regtest/bench/completion_bench_test.go b/gopls/internal/regtest/bench/completion_bench_test.go deleted file mode 100644 index f9b8445891d..00000000000 --- a/gopls/internal/regtest/bench/completion_bench_test.go +++ /dev/null @@ -1,187 +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 bench - -import ( - "flag" - "fmt" - "strings" - "testing" - - . "golang.org/x/tools/internal/lsp/regtest" - - "golang.org/x/tools/internal/lsp/fake" -) - -// dummyCompletionFunction to test manually configured completion using CLI. -func dummyCompletionFunction() { const s = "placeholder"; fmt.Printf("%s", s) } - -type completionBenchOptions struct { - workdir, file, locationRegexp string - printResults bool - // hook to run edits before initial completion, not supported for manually - // configured completions. - preCompletionEdits func(*Env) -} - -var completionOptions = completionBenchOptions{} - -func init() { - flag.StringVar(&completionOptions.workdir, "completion_workdir", "", "directory to run completion benchmarks in") - flag.StringVar(&completionOptions.file, "completion_file", "", "relative path to the file to complete in") - flag.StringVar(&completionOptions.locationRegexp, "completion_regexp", "", "regexp location to complete at") - flag.BoolVar(&completionOptions.printResults, "completion_print_results", false, "whether to print completion results") -} - -func benchmarkCompletion(options completionBenchOptions, t *testing.T) { - if completionOptions.workdir == "" { - t.Skip("-completion_workdir not configured, skipping benchmark") - } - - opts := stressTestOptions(options.workdir) - - // Completion gives bad results if IWL is not yet complete, so we must await - // it first (and therefore need hooks). - opts = append(opts, SkipHooks(false)) - - WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) { - env.OpenFile(options.file) - - // Run edits required for this completion. - if options.preCompletionEdits != nil { - options.preCompletionEdits(env) - } - - // Run a completion to make sure the system is warm. - pos := env.RegexpSearch(options.file, options.locationRegexp) - completions := env.Completion(options.file, pos) - - if options.printResults { - fmt.Println("Results:") - for i := 0; i < len(completions.Items); i++ { - fmt.Printf("\t%d. %v\n", i, completions.Items[i]) - } - } - - results := testing.Benchmark(func(b *testing.B) { - for i := 0; i < b.N; i++ { - env.Completion(options.file, pos) - } - }) - - printBenchmarkResults(results) - }) -} - -// endPosInBuffer returns the position for last character in the buffer for -// the given file. -func endPosInBuffer(env *Env, name string) fake.Pos { - buffer := env.Editor.BufferText(name) - lines := strings.Split(buffer, "\n") - numLines := len(lines) - - return fake.Pos{ - Line: numLines - 1, - Column: len([]rune(lines[numLines-1])), - } -} - -// Benchmark completion at a specified file and location. When no CLI options -// are specified, this test is skipped. -// To Run (from x/tools/gopls) against the dummy function above: -// -// go test -v ./internal/regtest/bench -run=TestBenchmarkConfiguredCompletion -// -completion_workdir="$HOME/Developer/tools" -// -completion_file="gopls/internal/regtest/completion_bench_test.go" -// -completion_regexp="dummyCompletionFunction.*fmt\.Printf\(\"%s\", s(\))" -func TestBenchmarkConfiguredCompletion(t *testing.T) { - benchmarkCompletion(completionOptions, t) -} - -// To run (from x/tools/gopls): -// go test -v ./internal/regtest/bench -run TestBenchmark<>Completion -// -completion_workdir="$HOME/Developer/tools" -// where <> is one of the tests below. completion_workdir should be path to -// x/tools on your system. - -// Benchmark struct completion in tools codebase. -func TestBenchmarkStructCompletion(t *testing.T) { - file := "internal/lsp/cache/session.go" - - preCompletionEdits := func(env *Env) { - env.OpenFile(file) - originalBuffer := env.Editor.BufferText(file) - env.EditBuffer(file, fake.Edit{ - End: endPosInBuffer(env, file), - Text: originalBuffer + "\nvar testVariable map[string]bool = Session{}.\n", - }) - } - - benchmarkCompletion(completionBenchOptions{ - workdir: completionOptions.workdir, - file: file, - locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`, - preCompletionEdits: preCompletionEdits, - printResults: completionOptions.printResults, - }, t) -} - -// Benchmark import completion in tools codebase. -func TestBenchmarkImportCompletion(t *testing.T) { - benchmarkCompletion(completionBenchOptions{ - workdir: completionOptions.workdir, - file: "internal/lsp/source/completion/completion.go", - locationRegexp: `go\/()`, - printResults: completionOptions.printResults, - }, t) -} - -// Benchmark slice completion in tools codebase. -func TestBenchmarkSliceCompletion(t *testing.T) { - file := "internal/lsp/cache/session.go" - - preCompletionEdits := func(env *Env) { - env.OpenFile(file) - originalBuffer := env.Editor.BufferText(file) - env.EditBuffer(file, fake.Edit{ - End: endPosInBuffer(env, file), - Text: originalBuffer + "\nvar testVariable []byte = \n", - }) - } - - benchmarkCompletion(completionBenchOptions{ - workdir: completionOptions.workdir, - file: file, - locationRegexp: `var testVariable \[\]byte (=)`, - preCompletionEdits: preCompletionEdits, - printResults: completionOptions.printResults, - }, t) -} - -// Benchmark deep completion in function call in tools codebase. -func TestBenchmarkFuncDeepCompletion(t *testing.T) { - file := "internal/lsp/source/completion/completion.go" - fileContent := ` -func (c *completer) _() { - c.inference.kindMatches(c.) -} -` - preCompletionEdits := func(env *Env) { - env.OpenFile(file) - originalBuffer := env.Editor.BufferText(file) - env.EditBuffer(file, fake.Edit{ - End: endPosInBuffer(env, file), - Text: originalBuffer + fileContent, - }) - } - - benchmarkCompletion(completionBenchOptions{ - workdir: completionOptions.workdir, - file: file, - locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, - preCompletionEdits: preCompletionEdits, - printResults: completionOptions.printResults, - }, t) -} diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go new file mode 100644 index 00000000000..7b833b6e7c4 --- /dev/null +++ b/gopls/internal/regtest/bench/completion_test.go @@ -0,0 +1,204 @@ +// 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 bench + +import ( + "context" + "fmt" + "strings" + "testing" + + . "golang.org/x/tools/gopls/internal/lsp/regtest" + + "golang.org/x/tools/gopls/internal/lsp/fake" +) + +type completionBenchOptions struct { + file, locationRegexp string + + // Hooks to run edits before initial completion + setup func(*Env) // run before the benchmark starts + beforeCompletion func(*Env) // run before each completion +} + +func benchmarkCompletion(options completionBenchOptions, b *testing.B) { + dir := benchmarkDir() + + // Use a new environment for each test, to avoid any existing state from the + // previous session. + sandbox, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{ + Settings: map[string]interface{}{ + "completionBudget": "1m", // arbitrary long completion budget + }, + }) + if err != nil { + b.Fatal(err) + } + ctx := context.Background() + defer func() { + if err := editor.Close(ctx); err != nil { + b.Errorf("closing editor: %v", err) + } + }() + + env := &Env{ + T: b, + Ctx: ctx, + Editor: editor, + Sandbox: sandbox, + Awaiter: awaiter, + } + + // Run edits required for this completion. + if options.setup != nil { + options.setup(env) + } + + // Run a completion to make sure the system is warm. + pos := env.RegexpSearch(options.file, options.locationRegexp) + completions := env.Completion(options.file, pos) + + if testing.Verbose() { + fmt.Println("Results:") + for i := 0; i < len(completions.Items); i++ { + fmt.Printf("\t%d. %v\n", i, completions.Items[i]) + } + } + + b.ResetTimer() + + // Use a subtest to ensure that benchmarkCompletion does not itself get + // executed multiple times (as it is doing expensive environment + // initialization). + b.Run("completion", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if options.beforeCompletion != nil { + options.beforeCompletion(env) + } + env.Completion(options.file, pos) + } + }) +} + +// endPosInBuffer returns the position for last character in the buffer for +// the given file. +func endPosInBuffer(env *Env, name string) fake.Pos { + buffer := env.Editor.BufferText(name) + lines := strings.Split(buffer, "\n") + numLines := len(lines) + + return fake.Pos{ + Line: numLines - 1, + Column: len([]rune(lines[numLines-1])), + } +} + +// Benchmark struct completion in tools codebase. +func BenchmarkStructCompletion(b *testing.B) { + file := "internal/lsp/cache/session.go" + + setup := func(env *Env) { + env.OpenFile(file) + originalBuffer := env.Editor.BufferText(file) + env.EditBuffer(file, fake.Edit{ + End: endPosInBuffer(env, file), + Text: originalBuffer + "\nvar testVariable map[string]bool = Session{}.\n", + }) + } + + benchmarkCompletion(completionBenchOptions{ + file: file, + locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`, + setup: setup, + }, b) +} + +// Benchmark import completion in tools codebase. +func BenchmarkImportCompletion(b *testing.B) { + const file = "internal/lsp/source/completion/completion.go" + benchmarkCompletion(completionBenchOptions{ + file: file, + locationRegexp: `go\/()`, + setup: func(env *Env) { env.OpenFile(file) }, + }, b) +} + +// Benchmark slice completion in tools codebase. +func BenchmarkSliceCompletion(b *testing.B) { + file := "internal/lsp/cache/session.go" + + setup := func(env *Env) { + env.OpenFile(file) + originalBuffer := env.Editor.BufferText(file) + env.EditBuffer(file, fake.Edit{ + End: endPosInBuffer(env, file), + Text: originalBuffer + "\nvar testVariable []byte = \n", + }) + } + + benchmarkCompletion(completionBenchOptions{ + file: file, + locationRegexp: `var testVariable \[\]byte (=)`, + setup: setup, + }, b) +} + +// Benchmark deep completion in function call in tools codebase. +func BenchmarkFuncDeepCompletion(b *testing.B) { + file := "internal/lsp/source/completion/completion.go" + fileContent := ` +func (c *completer) _() { + c.inference.kindMatches(c.) +} +` + setup := func(env *Env) { + env.OpenFile(file) + originalBuffer := env.Editor.BufferText(file) + env.EditBuffer(file, fake.Edit{ + End: endPosInBuffer(env, file), + Text: originalBuffer + fileContent, + }) + } + + benchmarkCompletion(completionBenchOptions{ + file: file, + locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, + setup: setup, + }, b) +} + +// Benchmark completion following an arbitrary edit. +// +// Edits force type-checked packages to be invalidated, so we want to measure +// how long it takes before completion results are available. +func BenchmarkCompletionFollowingEdit(b *testing.B) { + file := "internal/lsp/source/completion/completion2.go" + fileContent := ` +package completion + +func (c *completer) _() { + c.inference.kindMatches(c.) + // __MAGIC_STRING_1 +} +` + setup := func(env *Env) { + env.CreateBuffer(file, fileContent) + } + + n := 1 + beforeCompletion := func(env *Env) { + old := fmt.Sprintf("__MAGIC_STRING_%d", n) + new := fmt.Sprintf("__MAGIC_STRING_%d", n+1) + n++ + env.RegexpReplace(file, old, new) + } + + benchmarkCompletion(completionBenchOptions{ + file: file, + locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, + setup: setup, + beforeCompletion: beforeCompletion, + }, b) +} diff --git a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go new file mode 100644 index 00000000000..5fd5e9c577b --- /dev/null +++ b/gopls/internal/regtest/bench/didchange_test.go @@ -0,0 +1,38 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "fmt" + "testing" + + "golang.org/x/tools/gopls/internal/lsp/fake" +) + +// BenchmarkDidChange benchmarks modifications of a single file by making +// synthetic modifications in a comment. It controls pacing by waiting for the +// server to actually start processing the didChange notification before +// proceeding. Notably it does not wait for diagnostics to complete. +// +// Uses -workdir and -file to control where the edits occur. +func BenchmarkDidChange(b *testing.B) { + env := benchmarkEnv(b) + env.OpenFile(*file) + env.Await(env.DoneWithOpen()) + + // Insert the text we'll be modifying at the top of the file. + env.EditBuffer(*file, fake.Edit{Text: "// __REGTEST_PLACEHOLDER_0__\n"}) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + env.EditBuffer(*file, fake.Edit{ + Start: fake.Pos{Line: 0, Column: 0}, + End: fake.Pos{Line: 1, Column: 0}, + // Increment the placeholder text, to ensure cache misses. + Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1), + }) + env.Await(env.StartedChange()) + } +} diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go new file mode 100644 index 00000000000..baa92fc4b4d --- /dev/null +++ b/gopls/internal/regtest/bench/iwl_test.go @@ -0,0 +1,36 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "context" + "testing" + + "golang.org/x/tools/gopls/internal/lsp/fake" + . "golang.org/x/tools/gopls/internal/lsp/regtest" +) + +// BenchmarkInitialWorkspaceLoad benchmarks the initial workspace load time for +// a new editing session. +func BenchmarkInitialWorkspaceLoad(b *testing.B) { + dir := benchmarkDir() + b.ResetTimer() + + ctx := context.Background() + for i := 0; i < b.N; i++ { + _, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{}) + if err != nil { + b.Fatal(err) + } + if err := awaiter.Await(ctx, InitialWorkspaceLoad); err != nil { + b.Fatal(err) + } + b.StopTimer() + if err := editor.Close(ctx); err != nil { + b.Fatal(err) + } + b.StartTimer() + } +} diff --git a/gopls/internal/regtest/bench/mem_test.go b/gopls/internal/regtest/bench/mem_test.go new file mode 100644 index 00000000000..19626785acc --- /dev/null +++ b/gopls/internal/regtest/bench/mem_test.go @@ -0,0 +1,39 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "runtime" + "testing" +) + +// TestPrintMemStats measures the memory usage of loading a project. +// It uses the same -didchange_dir flag as above. +// Always run it in isolation since it measures global heap usage. +// +// Kubernetes example: +// +// $ go test -v -run=TestPrintMemStats -workdir=$HOME/w/kubernetes +// TotalAlloc: 5766 MB +// HeapAlloc: 1984 MB +// +// Both figures exhibit variance of less than 1%. +func TestPrintMemStats(t *testing.T) { + // This test only makes sense when run in isolation, so for now it is + // manually skipped. + // + // TODO(rfindley): figure out a better way to capture memstats as a benchmark + // metric. + t.Skip("unskip to run this test manually") + + _ = benchmarkEnv(t) + + runtime.GC() + runtime.GC() + var mem runtime.MemStats + runtime.ReadMemStats(&mem) + t.Logf("TotalAlloc:\t%d MB", mem.TotalAlloc/1e6) + t.Logf("HeapAlloc:\t%d MB", mem.HeapAlloc/1e6) +} diff --git a/gopls/internal/regtest/bench/stress_test.go b/gopls/internal/regtest/bench/stress_test.go index f7e59faf97f..b1198b43338 100644 --- a/gopls/internal/regtest/bench/stress_test.go +++ b/gopls/internal/regtest/bench/stress_test.go @@ -11,56 +11,83 @@ import ( "testing" "time" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/lsprpc" + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/jsonrpc2/servertest" ) -// Pilosa is a repository that has historically caused significant memory -// problems for Gopls. We use it for a simple stress test that types -// arbitrarily in a file with lots of dependents. +// github.com/pilosa/pilosa is a repository that has historically caused +// significant memory problems for Gopls. We use it for a simple stress test +// that types arbitrarily in a file with lots of dependents. var pilosaPath = flag.String("pilosa_path", "", "Path to a directory containing "+ "github.com/pilosa/pilosa, for stress testing. Do not set this unless you "+ "know what you're doing!") -func stressTestOptions(dir string) []RunOption { - opts := benchmarkOptions(dir) - opts = append(opts, SkipHooks(true), DebugAddress(":8087")) - return opts -} - func TestPilosaStress(t *testing.T) { + // TODO(rfindley): revisit this test and make it is hermetic: it should check + // out pilosa into a directory. + // + // Note: This stress test has not been run recently, and may no longer + // function properly. if *pilosaPath == "" { t.Skip("-pilosa_path not configured") } - opts := stressTestOptions(*pilosaPath) - WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) { - files := []string{ - "cmd.go", - "internal/private.pb.go", - "roaring/roaring.go", - "roaring/roaring_internal_test.go", - "server/handler_test.go", - } - for _, file := range files { - env.OpenFile(file) + sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ + Workdir: *pilosaPath, + GOPROXY: "https://proxy.golang.org", + }) + if err != nil { + t.Fatal(err) + } + + server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false) + ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream) + ctx := context.Background() + + editor, err := fake.NewEditor(sandbox, fake.EditorConfig{}).Connect(ctx, ts, fake.ClientHooks{}) + if err != nil { + t.Fatal(err) + } + + files := []string{ + "cmd.go", + "internal/private.pb.go", + "roaring/roaring.go", + "roaring/roaring_internal_test.go", + "server/handler_test.go", + } + for _, file := range files { + if err := editor.OpenFile(ctx, file); err != nil { + t.Fatal(err) } - ctx, cancel := context.WithTimeout(env.Ctx, 10*time.Minute) - defer cancel() + } + ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) + defer cancel() - i := 1 - // MagicNumber is an identifier that occurs in roaring.go. Just change it - // arbitrarily. - env.RegexpReplace("roaring/roaring.go", "MagicNumber", fmt.Sprintf("MagicNumber%d", 1)) - for { - select { - case <-ctx.Done(): - return - default: - } - env.RegexpReplace("roaring/roaring.go", fmt.Sprintf("MagicNumber%d", i), fmt.Sprintf("MagicNumber%d", i+1)) - time.Sleep(20 * time.Millisecond) - i++ + i := 1 + // MagicNumber is an identifier that occurs in roaring.go. Just change it + // arbitrarily. + if err := editor.RegexpReplace(ctx, "roaring/roaring.go", "MagicNumber", fmt.Sprintf("MagicNumber%d", 1)); err != nil { + t.Fatal(err) + } + for { + select { + case <-ctx.Done(): + return + default: } - }) + if err := editor.RegexpReplace(ctx, "roaring/roaring.go", fmt.Sprintf("MagicNumber%d", i), fmt.Sprintf("MagicNumber%d", i+1)); err != nil { + t.Fatal(err) + } + // Simulate (very fast) typing. + // + // Typing 80 wpm ~150ms per keystroke. + time.Sleep(150 * time.Millisecond) + i++ + } } diff --git a/gopls/internal/regtest/bench/workspace_symbols_test.go b/gopls/internal/regtest/bench/workspace_symbols_test.go new file mode 100644 index 00000000000..fccc8182997 --- /dev/null +++ b/gopls/internal/regtest/bench/workspace_symbols_test.go @@ -0,0 +1,35 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bench + +import ( + "flag" + "fmt" + "testing" +) + +var symbolQuery = flag.String("symbol_query", "test", "symbol query to use in benchmark") + +// BenchmarkWorkspaceSymbols benchmarks the time to execute a workspace symbols +// request (controlled by the -symbol_query flag). +func BenchmarkWorkspaceSymbols(b *testing.B) { + env := benchmarkEnv(b) + + // Make an initial symbol query to warm the cache. + symbols := env.WorkspaceSymbol(*symbolQuery) + + if testing.Verbose() { + fmt.Println("Results:") + for i := 0; i < len(symbols); i++ { + fmt.Printf("\t%d. %s (%s)\n", i, symbols[i].Name, symbols[i].ContainerName) + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + env.WorkspaceSymbol(*symbolQuery) + } +} diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index a64f9c480ae..096d572f906 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -6,18 +6,15 @@ package codelens import ( "fmt" - "runtime" - "strings" "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -63,9 +60,7 @@ const ( for _, test := range tests { t.Run(test.label, func(t *testing.T) { WithOptions( - EditorConfig{ - CodeLenses: test.enabled, - }, + Settings{"codelenses": test.enabled}, ).Run(t, workspace, func(t *testing.T, env *Env) { env.OpenFile("lib.go") lens := env.CodeLens("lib.go") @@ -80,7 +75,8 @@ const ( // This test confirms the full functionality of the code lenses for updating // dependencies in a go.mod file. It checks for the code lens that suggests // an update and then executes the command associated with that code lens. A -// regression test for golang/go#39446. +// regression test for golang/go#39446. It also checks that these code lenses +// only affect the diagnostics and contents of the containing go.mod file. func TestUpgradeCodelens(t *testing.T) { const proxyWithLatest = ` -- golang.org/x/hello@v1.3.3/go.mod -- @@ -102,30 +98,64 @@ var Goodbye error ` const shouldUpdateDep = ` --- go.mod -- -module mod.com +-- go.work -- +go 1.18 + +use ( + ./a + ./b +) +-- a/go.mod -- +module mod.com/a go 1.14 require golang.org/x/hello v1.2.3 --- go.sum -- +-- a/go.sum -- golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= --- main.go -- +-- a/main.go -- package main import "golang.org/x/hello/hi" +func main() { + _ = hi.Goodbye +} +-- b/go.mod -- +module mod.com/b + +go 1.14 + +require golang.org/x/hello v1.2.3 +-- b/go.sum -- +golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= +golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= +-- b/main.go -- +package main + +import ( + "golang.org/x/hello/hi" +) + func main() { _ = hi.Goodbye } ` - const wantGoMod = `module mod.com + const wantGoModA = `module mod.com/a go 1.14 require golang.org/x/hello v1.3.3 +` + // Applying the diagnostics or running the codelenses for a/go.mod + // should not change the contents of b/go.mod + const wantGoModB = `module mod.com/b + +go 1.14 + +require golang.org/x/hello v1.2.3 ` for _, commandTitle := range []string{ @@ -136,10 +166,11 @@ require golang.org/x/hello v1.3.3 WithOptions( ProxyFiles(proxyWithLatest), ).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { - env.OpenFile("go.mod") + env.OpenFile("a/go.mod") + env.OpenFile("b/go.mod") var lens protocol.CodeLens var found bool - for _, l := range env.CodeLens("go.mod") { + for _, l := range env.CodeLens("a/go.mod") { if l.Command.Title == commandTitle { lens = l found = true @@ -155,8 +186,11 @@ require golang.org/x/hello v1.3.3 t.Fatal(err) } env.Await(env.DoneWithChangeWatchedFiles()) - if got := env.Editor.BufferText("go.mod"); got != wantGoMod { - t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(t, wantGoMod, got)) + if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { + t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) + } + if got := env.Editor.BufferText("b/go.mod"); got != wantGoModB { + t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) } }) }) @@ -165,22 +199,38 @@ require golang.org/x/hello v1.3.3 t.Run(fmt.Sprintf("Upgrade individual dependency vendoring=%v", vendoring), func(t *testing.T) { WithOptions(ProxyFiles(proxyWithLatest)).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { if vendoring { - env.RunGoCommand("mod", "vendor") + env.RunGoCommandInDir("a", "mod", "vendor") } env.Await(env.DoneWithChangeWatchedFiles()) - env.OpenFile("go.mod") - env.ExecuteCodeLensCommand("go.mod", command.CheckUpgrades) + env.OpenFile("a/go.mod") + env.OpenFile("b/go.mod") + env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades) d := &protocol.PublishDiagnosticsParams{} env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("go.mod", `require`, "can be upgraded"), - ReadDiagnostics("go.mod", d), + env.DiagnosticAtRegexpWithMessage("a/go.mod", `require`, "can be upgraded"), + ReadDiagnostics("a/go.mod", d), + // We do not want there to be a diagnostic for b/go.mod, + // but there may be some subtlety in timing here, where this + // should always succeed, but may not actually test the correct + // behavior. + env.NoDiagnosticAtRegexp("b/go.mod", `require`), ), ) - env.ApplyQuickFixes("go.mod", d.Diagnostics) + // Check for upgrades in b/go.mod and then clear them. + env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades) + env.Await(env.DiagnosticAtRegexpWithMessage("b/go.mod", `require`, "can be upgraded")) + env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics) + env.Await(EmptyDiagnostics("b/go.mod")) + + // Apply the diagnostics to a/go.mod. + env.ApplyQuickFixes("a/go.mod", d.Diagnostics) env.Await(env.DoneWithChangeWatchedFiles()) - if got := env.Editor.BufferText("go.mod"); got != wantGoMod { - t.Fatalf("go.mod upgrade failed:\n%s", tests.Diff(t, wantGoMod, got)) + if got := env.Editor.BufferText("a/go.mod"); got != wantGoModA { + t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) + } + if got := env.Editor.BufferText("b/go.mod"); got != wantGoModB { + t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) } }) }) @@ -242,7 +292,7 @@ go 1.14 require golang.org/x/hello v1.0.0 ` if got != wantGoMod { - t.Fatalf("go.mod tidy failed:\n%s", tests.Diff(t, wantGoMod, got)) + t.Fatalf("go.mod tidy failed:\n%s", compare.Text(wantGoMod, got)) } }) } @@ -286,70 +336,3 @@ func Foo() { env.Await(EmptyDiagnostics("cgo.go")) }) } - -func TestGCDetails(t *testing.T) { - testenv.NeedsGo1Point(t, 15) - 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( - EditorConfig{ - 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) - d := &protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - DiagnosticAt("main.go", 5, 13), - ReadDiagnostics("main.go", d), - ), - ) - // Confirm that the diagnostics come from the gc details code lens. - var found bool - for _, d := range d.Diagnostics { - if d.Severity != protocol.SeverityInformation { - t.Fatalf("unexpected diagnostic severity %v, wanted Information", d.Severity) - } - if strings.Contains(d.Message, "42 escapes") { - found = true - } - } - if !found { - t.Fatalf(`expected to find diagnostic with message "escape(42 escapes to heap)", found none`) - } - - // Editing a buffer should cause gc_details diagnostics to disappear, since - // they only apply to saved buffers. - env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, "\n\n")) - env.Await(EmptyDiagnostics("main.go")) - - // Saving a buffer should re-format back to the original state, and - // re-enable the gc_details diagnostics. - env.SaveBuffer("main.go") - env.Await(DiagnosticAt("main.go", 5, 13)) - - // Toggle the GC details code lens again so now it should be off. - env.ExecuteCodeLensCommand("main.go", command.GCDetails) - env.Await( - EmptyDiagnostics("main.go"), - ) - }) -} diff --git a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go new file mode 100644 index 00000000000..762694ac718 --- /dev/null +++ b/gopls/internal/regtest/codelens/gcdetails_test.go @@ -0,0 +1,137 @@ +// 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" + "strings" + "testing" + + "golang.org/x/tools/internal/testenv" + + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" +) + +func TestGCDetails_Toggle(t *testing.T) { + testenv.NeedsGo1Point(t, 15) + 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) + d := &protocol.PublishDiagnosticsParams{} + env.Await( + OnceMet( + DiagnosticAt("main.go", 5, 13), + ReadDiagnostics("main.go", d), + ), + ) + // Confirm that the diagnostics come from the gc details code lens. + var found bool + for _, d := range d.Diagnostics { + if d.Severity != protocol.SeverityInformation { + t.Fatalf("unexpected diagnostic severity %v, wanted Information", d.Severity) + } + if strings.Contains(d.Message, "42 escapes") { + found = true + } + } + if !found { + t.Fatalf(`expected to find diagnostic with message "escape(42 escapes to heap)", found none`) + } + + // Editing a buffer should cause gc_details diagnostics to disappear, since + // they only apply to saved buffers. + env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, "\n\n")) + env.Await(EmptyDiagnostics("main.go")) + + // Saving a buffer should re-format back to the original state, and + // re-enable the gc_details diagnostics. + env.SaveBuffer("main.go") + env.Await(DiagnosticAt("main.go", 5, 13)) + + // Toggle the GC details code lens again so now it should be off. + env.ExecuteCodeLensCommand("main.go", command.GCDetails) + env.Await( + EmptyDiagnostics("main.go"), + ) + }) +} + +// Test for the crasher in golang/go#54199 +func TestGCDetails_NewFile(t *testing.T) { + bug.PanicOnBugs = false + // It appears that older Go versions don't even see p.go from the initial + // workspace load. + testenv.NeedsGo1Point(t, 15) + 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", "") + + const gcDetailsCommand = "gopls." + string(command.GCDetails) + + hasGCDetails := func() bool { + lenses := env.CodeLens("p_test.go") // should not crash + for _, lens := range lenses { + if lens.Command.Command == gcDetailsCommand { + 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/regtest/completion/completion18_test.go b/gopls/internal/regtest/completion/completion18_test.go index 9683e30c828..7c532529c7b 100644 --- a/gopls/internal/regtest/completion/completion18_test.go +++ b/gopls/internal/regtest/completion/completion18_test.go @@ -10,7 +10,7 @@ package completion import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // test generic receivers diff --git a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go index 1ffb0000d3b..eaf4327a686 100644 --- a/gopls/internal/regtest/completion/completion_test.go +++ b/gopls/internal/regtest/completion/completion_test.go @@ -10,11 +10,11 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -529,7 +529,7 @@ func main() { } ` WithOptions( - EditorConfig{WindowsLineEndings: true}, + WindowsLineEndings(), ).Run(t, src, func(t *testing.T, env *Env) { // Trigger unimported completions for the example.com/blah package. env.OpenFile("main.go") diff --git a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go index 2674d555c5a..56e26a235bf 100644 --- a/gopls/internal/regtest/completion/postfix_snippet_test.go +++ b/gopls/internal/regtest/completion/postfix_snippet_test.go @@ -8,13 +8,10 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/source" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestPostfixSnippetCompletion(t *testing.T) { - t.Skipf("skipping test due to suspected synchronization bug; see https://go.dev/issue/50707") - const mod = ` -- go.mod -- module mod.com @@ -264,6 +261,27 @@ for k := range foo { keys = append(keys, k) } +} +`, + }, + { + name: "channel_range", + before: ` +package foo + +func _() { + foo := make(chan int) + foo.range +} +`, + after: ` +package foo + +func _() { + foo := make(chan int) + for e := range foo { + $0 +} } `, }, @@ -379,7 +397,7 @@ func _() { before: ` package foo -func foo() []string { +func foo() []string { x := "test" return x.split }`, @@ -388,7 +406,7 @@ package foo import "strings" -func foo() []string { +func foo() []string { x := "test" return strings.Split(x, "$0") }`, @@ -414,16 +432,20 @@ func foo() string { }, } - r := WithOptions(Options(func(o *source.Options) { - o.ExperimentalPostfixCompletions = true - })) + r := WithOptions( + Settings{ + "experimentalPostfixCompletions": true, + }, + ) r.Run(t, mod, func(t *testing.T, env *Env) { + env.CreateBuffer("foo.go", "") + for _, c := range cases { t.Run(c.name, func(t *testing.T) { c.before = strings.Trim(c.before, "\n") c.after = strings.Trim(c.after, "\n") - env.CreateBuffer("foo.go", c.before) + env.SetBufferContent("foo.go", c.before) pos := env.RegexpSearch("foo.go", "\n}") completions := env.Completion("foo.go", pos) diff --git a/gopls/internal/regtest/debug/debug_test.go b/gopls/internal/regtest/debug/debug_test.go index d60b3f780d7..f8efb8f5d30 100644 --- a/gopls/internal/regtest/debug/debug_test.go +++ b/gopls/internal/regtest/debug/debug_test.go @@ -8,8 +8,8 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" ) func TestMain(m *testing.M) { @@ -20,12 +20,8 @@ func TestBugNotification(t *testing.T) { // Verify that a properly configured session gets notified of a bug on the // server. WithOptions( - Modes(Singleton), // must be in-process to receive the bug report below - EditorConfig{ - Settings: map[string]interface{}{ - "showBugReports": true, - }, - }, + Modes(Default), // must be in-process to receive the bug report below + Settings{"showBugReports": true}, ).Run(t, "", func(t *testing.T, env *Env) { const desc = "got a bug" bug.Report(desc, nil) diff --git a/gopls/internal/regtest/diagnostics/analysis_test.go b/gopls/internal/regtest/diagnostics/analysis_test.go new file mode 100644 index 00000000000..56ee23f3f85 --- /dev/null +++ b/gopls/internal/regtest/diagnostics/analysis_test.go @@ -0,0 +1,54 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diagnostics + +import ( + "testing" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" +) + +// Test for the timeformat analyzer, following golang/vscode-go#2406. +// +// This test checks that applying the suggested fix from the analyzer resolves +// the diagnostic warning. +func TestTimeFormatAnalyzer(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- main.go -- +package main + +import ( + "fmt" + "time" +) + +func main() { + now := time.Now() + fmt.Println(now.Format("2006-02-01")) +}` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + + var d protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("main.go", "2006-02-01"), + ReadDiagnostics("main.go", &d), + ), + ) + + env.ApplyQuickFixes("main.go", d.Diagnostics) + env.Await( + EmptyDiagnostics("main.go"), + ) + }) +} diff --git a/gopls/internal/regtest/diagnostics/builtin_test.go b/gopls/internal/regtest/diagnostics/builtin_test.go index 775e7ec0b14..8de47bcd946 100644 --- a/gopls/internal/regtest/diagnostics/builtin_test.go +++ b/gopls/internal/regtest/diagnostics/builtin_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestIssue44866(t *testing.T) { diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index 6f5db4cd419..18c022cb2a5 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -11,12 +11,12 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -298,7 +298,7 @@ func Hello() { t.Run("without workspace module", func(t *testing.T) { WithOptions( - Modes(Singleton), + Modes(Default), ).Run(t, noMod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), @@ -384,7 +384,7 @@ func main() {} // completed. OnceMet( env.DoneWithChange(), - NoDiagnostics("a.go"), + EmptyDiagnostics("a.go"), ), ) }) @@ -471,12 +471,11 @@ func _() { } ` WithOptions( - EditorConfig{ - Env: map[string]string{ - "GOPATH": "", - "GO111MODULE": "off", - }, - }).Run(t, files, func(t *testing.T, env *Env) { + EnvVars{ + "GOPATH": "", + "GO111MODULE": "off", + }, + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.Await(env.DiagnosticAtRegexp("main.go", "fmt")) env.SaveBuffer("main.go") @@ -500,8 +499,9 @@ package x var X = 0 ` - editorConfig := EditorConfig{Env: map[string]string{"GOFLAGS": "-tags=foo"}} - WithOptions(editorConfig).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + EnvVars{"GOFLAGS": "-tags=foo"}, + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OrganizeImports("main.go") env.Await(EmptyDiagnostics("main.go")) @@ -543,7 +543,7 @@ func _() { // Expect a module/GOPATH error if there is an error in the file at startup. // Tests golang/go#37279. -func TestShowCriticalError_Issue37279(t *testing.T) { +func TestBrokenWorkspace_OutsideModule(t *testing.T) { const noModule = ` -- a.go -- package foo @@ -573,9 +573,9 @@ hi mom ` for _, go111module := range []string{"on", "off", ""} { t.Run(fmt.Sprintf("GO111MODULE_%v", go111module), func(t *testing.T) { - WithOptions(EditorConfig{ - Env: map[string]string{"GO111MODULE": go111module}, - }).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + EnvVars{"GO111MODULE": go111module}, + ).Run(t, files, func(t *testing.T, env *Env) { env.Await( NoOutstandingWork(), ) @@ -605,11 +605,7 @@ func main() { ` WithOptions( InGOPATH(), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "off", - }, - }, + EnvVars{"GO111MODULE": "off"}, ).Run(t, collision, func(t *testing.T, env *Env) { env.OpenFile("x/x.go") env.Await( @@ -622,7 +618,7 @@ func main() { env.RegexpReplace("x/x.go", `package x`, `package main`) env.Await(OnceMet( env.DoneWithChange(), - env.DiagnosticAtRegexpWithMessage("x/main.go", `fmt`, "undeclared name"))) + env.DiagnosticAtRegexp("x/main.go", `fmt`))) } }) } @@ -640,8 +636,6 @@ var ErrHelpWanted error // Test for golang/go#38211. func Test_Issue38211(t *testing.T) { - t.Skipf("Skipping flaky test: https://golang.org/issue/44098") - testenv.NeedsGo1Point(t, 14) const ardanLabs = ` -- go.mod -- @@ -707,7 +701,8 @@ func main() { // Test for golang/go#38207. func TestNewModule_Issue38207(t *testing.T) { - testenv.NeedsGo1Point(t, 14) + // Fails at Go 1.14 following CL 417576. Not investigated. + testenv.NeedsGo1Point(t, 15) const emptyFile = ` -- go.mod -- module mod.com @@ -762,15 +757,20 @@ func _() { env.OpenFile("a/a1.go") env.CreateBuffer("a/a2.go", ``) env.SaveBufferWithoutActions("a/a2.go") + // We can't use OnceMet here (at least, not easily) because the didSave + // races with the didChangeWatchedFiles. + // + // TODO(rfindley): add an AllOf expectation combinator, or an expectation + // that all notifications have been processed. env.Await( - OnceMet( - env.DoneWithSave(), - NoDiagnostics("a/a1.go"), - ), + EmptyDiagnostics("a/a1.go"), ) env.EditBuffer("a/a2.go", fake.NewEdit(0, 0, 0, 0, `package a`)) env.Await( - OnceMet(env.DoneWithChange(), NoDiagnostics("a/a1.go")), + OnceMet( + env.DoneWithChange(), + EmptyDiagnostics("a/a1.go"), + ), ) }) } @@ -878,7 +878,7 @@ func TestX(t *testing.T) { } func TestChangePackageName(t *testing.T) { - t.Skip("This issue hasn't been fixed yet. See golang.org/issue/41061.") + testenv.NeedsGo1Point(t, 16) // needs native overlay support const mod = ` -- go.mod -- @@ -893,15 +893,11 @@ package foo_ Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("foo/bar_test.go") env.RegexpReplace("foo/bar_test.go", "package foo_", "package foo_test") - env.SaveBuffer("foo/bar_test.go") env.Await( OnceMet( - env.DoneWithSave(), - NoDiagnostics("foo/bar_test.go"), - ), - OnceMet( - env.DoneWithSave(), - NoDiagnostics("foo/foo.go"), + env.DoneWithChange(), + EmptyOrNoDiagnostics("foo/bar_test.go"), + EmptyOrNoDiagnostics("foo/foo.go"), ), ) }) @@ -923,7 +919,7 @@ var _ = foo.Bar env.Await( OnceMet( env.DoneWithOpen(), - NoDiagnostics("_foo/x.go"), + EmptyDiagnostics("_foo/x.go"), )) }) } @@ -971,8 +967,6 @@ const C = a.A // This is a copy of the scenario_default/quickfix_empty_files.txt test from // govim. Reproduces golang/go#39646. func TestQuickFixEmptyFiles(t *testing.T) { - t.Skip("too flaky: golang/go#48773") - testenv.NeedsGo1Point(t, 15) const mod = ` @@ -1236,7 +1230,7 @@ func main() { }) WithOptions( WorkspaceFolders("a"), - LimitWorkspaceScope(), + Settings{"expandWorkspaceToModule": false}, ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.Await( @@ -1267,11 +1261,7 @@ func main() { ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "staticcheck": true, - }, - }, + Settings{"staticcheck": true}, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") var d protocol.PublishDiagnosticsParams @@ -1306,7 +1296,7 @@ func main() {} Run(t, dir, func(t *testing.T, env *Env) { env.OpenFile("main.go") env.OpenFile("other.go") - x := env.DiagnosticsFor("main.go") + x := env.Awaiter.DiagnosticsFor("main.go") if x == nil { t.Fatalf("expected 1 diagnostic, got none") } @@ -1314,7 +1304,7 @@ func main() {} t.Fatalf("main.go, got %d diagnostics, expected 1", len(x.Diagnostics)) } keep := x.Diagnostics[0] - y := env.DiagnosticsFor("other.go") + y := env.Awaiter.DiagnosticsFor("other.go") if len(y.Diagnostics) != 1 { t.Fatalf("other.go: got %d diagnostics, expected 1", len(y.Diagnostics)) } @@ -1344,8 +1334,8 @@ package a func main() { var x int } --- a/a_ignore.go -- -// +build ignore +-- a/a_exclude.go -- +// +build exclude package a @@ -1358,14 +1348,18 @@ func _() { env.Await( env.DiagnosticAtRegexp("a/a.go", "x"), ) - env.OpenFile("a/a_ignore.go") + env.OpenFile("a/a_exclude.go") env.Await( - DiagnosticAt("a/a_ignore.go", 2, 8), + DiagnosticAt("a/a_exclude.go", 2, 8), ) }) } func TestEnableAllExperiments(t *testing.T) { + // Before the oldest supported Go version, gopls sends a warning to upgrade + // Go, which fails the expectation below. + testenv.NeedsGo1Point(t, lsp.OldestSupportedGoVersion()) + const mod = ` -- go.mod -- module mod.com @@ -1381,12 +1375,15 @@ func b(c bytes.Buffer) { } ` WithOptions( - EditorConfig{ - AllExperiments: true, - }, + Settings{"allExperiments": true}, ).Run(t, mod, func(t *testing.T, env *Env) { // Confirm that the setting doesn't cause any warnings. - env.Await(NoShowMessage()) + env.Await( + OnceMet( + InitialWorkspaceLoad, + NoShownMessage(""), // empty substring to match any message + ), + ) }) } @@ -1495,11 +1492,7 @@ package foo_ WithOptions( ProxyFiles(proxy), InGOPATH(), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "off", - }, - }, + EnvVars{"GO111MODULE": "off"}, ).Run(t, contents, func(t *testing.T, env *Env) { // Simulate typing character by character. env.OpenFile("foo/foo_test.go") @@ -1564,7 +1557,7 @@ func Hello() { } -- go.mod -- module mod.com --- main.go -- +-- cmd/main.go -- package main import "mod.com/bob" @@ -1574,11 +1567,12 @@ func main() { } ` Run(t, mod, func(t *testing.T, env *Env) { + env.Await(FileWatchMatching("bob")) env.RemoveWorkspaceFile("bob") env.Await( - env.DiagnosticAtRegexp("main.go", `"mod.com/bob"`), + env.DiagnosticAtRegexp("cmd/main.go", `"mod.com/bob"`), EmptyDiagnostics("bob/bob.go"), - RegistrationMatching("didChangeWatchedFiles"), + NoFileWatchMatching("bob"), ) }) } @@ -1698,10 +1692,8 @@ import ( t.Run("GOPATH", func(t *testing.T) { WithOptions( InGOPATH(), - EditorConfig{ - Env: map[string]string{"GO111MODULE": "off"}, - }, - Modes(Singleton), + EnvVars{"GO111MODULE": "off"}, + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexpWithMessage("main.go", `"nosuchpkg"`, `cannot find package "nosuchpkg" in any of`), @@ -1710,65 +1702,6 @@ import ( }) } -func TestMultipleModules_Warning(t *testing.T) { - const modules = ` --- a/go.mod -- -module a.com - -go 1.12 --- a/a.go -- -package a --- b/go.mod -- -module b.com - -go 1.12 --- b/b.go -- -package b -` - for _, go111module := range []string{"on", "auto"} { - t.Run("GO111MODULE="+go111module, func(t *testing.T) { - WithOptions( - Modes(Singleton), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": go111module, - }, - }, - ).Run(t, modules, func(t *testing.T, env *Env) { - env.OpenFile("a/a.go") - env.OpenFile("b/go.mod") - env.Await( - env.DiagnosticAtRegexp("a/a.go", "package a"), - env.DiagnosticAtRegexp("b/go.mod", "module b.com"), - OutstandingWork(lsp.WorkspaceLoadFailure, "gopls requires a module at the root of your workspace."), - ) - }) - }) - } - - // Expect no warning if GO111MODULE=auto in a directory in GOPATH. - t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { - WithOptions( - Modes(Singleton), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "auto", - }, - }, - InGOPATH(), - ).Run(t, modules, func(t *testing.T, env *Env) { - env.OpenFile("a/a.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - NoDiagnostics("a/a.go"), - ), - NoOutstandingWork(), - ) - }) - }) -} - func TestNestedModules(t *testing.T) { const proxy = ` -- nested.com@v1.0.0/go.mod -- @@ -1815,7 +1748,7 @@ func helloHelper() {} ` WithOptions( ProxyFiles(proxy), - Modes(Singleton), + Modes(Default), ).Run(t, nested, func(t *testing.T, env *Env) { // Expect a diagnostic in a nested module. env.OpenFile("nested/hello/hello.go") @@ -1876,7 +1809,7 @@ var Bar = Foo Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo.go") - env.Await(env.DiagnosticAtRegexpWithMessage("bar.go", `Foo`, "undeclared name")) + env.Await(env.DiagnosticAtRegexp("bar.go", `Foo`)) env.RegexpReplace("foo.go", `\+build`, "") env.Await(EmptyDiagnostics("bar.go")) }) @@ -1907,15 +1840,15 @@ package main env.OpenFile("main.go") env.OpenFile("other.go") env.Await( - env.DiagnosticAtRegexpWithMessage("main.go", "asdf", "undeclared name"), - env.DiagnosticAtRegexpWithMessage("main.go", "fdas", "undeclared name"), + env.DiagnosticAtRegexp("main.go", "asdf"), + env.DiagnosticAtRegexp("main.go", "fdas"), ) env.SetBufferContent("other.go", "package main\n\nasdf") // The new diagnostic in other.go should not suppress diagnostics in main.go. env.Await( OnceMet( env.DiagnosticAtRegexpWithMessage("other.go", "asdf", "expected declaration"), - env.DiagnosticAtRegexpWithMessage("main.go", "asdf", "undeclared name"), + env.DiagnosticAtRegexp("main.go", "asdf"), ), ) }) @@ -1944,37 +1877,6 @@ package main }) } -// Tests golang/go#45075: A panic in fillreturns broke diagnostics. -// Expect an error log indicating that fillreturns panicked, as well type -// errors for the broken code. -func TestFillReturnsPanic(t *testing.T) { - // At tip, the panic no longer reproduces. - testenv.SkipAfterGo1Point(t, 16) - - const files = ` --- go.mod -- -module mod.com - -go 1.15 --- main.go -- -package main - -func foo() int { - return x, nil -} -` - Run(t, files, func(t *testing.T, env *Env) { - env.OpenFile("main.go") - env.Await( - OnceMet( - env.DoneWithOpen(), - LogMatching(protocol.Error, `.*analysis fillreturns.*panicked.*`, 1, true), - env.DiagnosticAtRegexpWithMessage("main.go", `return x`, "wrong number of return values"), - ), - ) - }) -} - // This test confirms that the view does not reinitialize when a go.mod file is // opened. func TestNoReinitialize(t *testing.T) { @@ -2026,10 +1928,8 @@ package a func Hello() {} ` WithOptions( - EditorConfig{ - ExperimentalUseInvalidMetadata: true, - }, - Modes(Singleton), + Settings{"experimentalUseInvalidMetadata": true}, + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.RegexpReplace("go.mod", "module mod.com", "modul mod.com") // break the go.mod file @@ -2082,12 +1982,10 @@ package main func _() {} ` WithOptions( - EditorConfig{ - ExperimentalUseInvalidMetadata: true, - }, + Settings{"experimentalUseInvalidMetadata": true}, // ExperimentalWorkspaceModule has a different failure mode for this // case. - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( OnceMet( @@ -2193,7 +2091,7 @@ func F[T C](_ T) { var d protocol.PublishDiagnosticsParams env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("main.go", `C`, "undeclared name"), + env.DiagnosticAtRegexp("main.go", `C`), ReadDiagnostics("main.go", &d), ), ) @@ -2220,7 +2118,7 @@ func F[T any](_ T) { var d protocol.PublishDiagnosticsParams env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameters require"), + env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), ReadDiagnostics("main.go", &d), ), ) @@ -2252,17 +2150,27 @@ func F[T any](_ T) { ` Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. var d protocol.PublishDiagnosticsParams + + // Once the initial workspace load is complete, we should have a diagnostic + // because generics are not supported at 1.16. env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameters require"), + InitialWorkspaceLoad, + env.DiagnosticAtRegexpWithMessage("main.go", `T any`, "type parameter"), ReadDiagnostics("main.go", &d), ), ) + // This diagnostic should have a quick fix to edit the go version. env.ApplyQuickFixes("main.go", d.Diagnostics) + // Once the edit is applied, the problematic diagnostics should be + // resolved. env.Await( - EmptyDiagnostics("main.go"), + OnceMet( + env.DoneWithChangeWatchedFiles(), // go.mod should have been quick-fixed + EmptyDiagnostics("main.go"), + ), ) }) } diff --git a/gopls/internal/regtest/diagnostics/invalidation_test.go b/gopls/internal/regtest/diagnostics/invalidation_test.go new file mode 100644 index 00000000000..2f0b173160c --- /dev/null +++ b/gopls/internal/regtest/diagnostics/invalidation_test.go @@ -0,0 +1,126 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diagnostics + +import ( + "fmt" + "testing" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" +) + +// Test for golang/go#50267: diagnostics should be re-sent after a file is +// opened. +func TestDiagnosticsAreResentAfterCloseOrOpen(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.16 +-- main.go -- +package main + +func _() { + x := 2 +} +` + Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. + env.OpenFile("main.go") + var afterOpen protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithOpen(), + ReadDiagnostics("main.go", &afterOpen), + ), + ) + env.CloseBuffer("main.go") + var afterClose protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithClose(), + ReadDiagnostics("main.go", &afterClose), + ), + ) + if afterOpen.Version == afterClose.Version { + t.Errorf("publishDiagnostics: got the same version after closing (%d) as after opening", afterOpen.Version) + } + env.OpenFile("main.go") + var afterReopen protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithOpen(), + ReadDiagnostics("main.go", &afterReopen), + ), + ) + if afterReopen.Version == afterClose.Version { + t.Errorf("pubslishDiagnostics: got the same version after reopening (%d) as after closing", afterClose.Version) + } + }) +} + +// Test for the "chattyDiagnostics" setting: we should get re-published +// diagnostics after every file change, even if diagnostics did not change. +func TestChattyDiagnostics(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.16 +-- main.go -- +package main + +func _() { + x := 2 +} + +// Irrelevant comment #0 +` + + WithOptions( + Settings{ + "chattyDiagnostics": true, + }, + ).Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. + + env.OpenFile("main.go") + var d protocol.PublishDiagnosticsParams + env.Await( + OnceMet( + env.DoneWithOpen(), + ReadDiagnostics("main.go", &d), + ), + ) + + if len(d.Diagnostics) != 1 { + t.Fatalf("len(Diagnostics) = %d, want 1", len(d.Diagnostics)) + } + msg := d.Diagnostics[0].Message + + for i := 0; i < 5; i++ { + before := d.Version + env.RegexpReplace("main.go", "Irrelevant comment #.", fmt.Sprintf("Irrelevant comment #%d", i)) + env.Await( + OnceMet( + env.DoneWithChange(), + ReadDiagnostics("main.go", &d), + ), + ) + + if d.Version == before { + t.Errorf("after change, got version %d, want new version", d.Version) + } + + // As a sanity check, make sure we have the same diagnostic. + if len(d.Diagnostics) != 1 { + t.Fatalf("len(Diagnostics) = %d, want 1", len(d.Diagnostics)) + } + newMsg := d.Diagnostics[0].Message + if newMsg != msg { + t.Errorf("after change, got message %q, want %q", newMsg, msg) + } + } + }) +} diff --git a/gopls/internal/regtest/diagnostics/undeclared_test.go b/gopls/internal/regtest/diagnostics/undeclared_test.go index 79f7d42675b..c3456fa2d58 100644 --- a/gopls/internal/regtest/diagnostics/undeclared_test.go +++ b/gopls/internal/regtest/diagnostics/undeclared_test.go @@ -7,8 +7,8 @@ package diagnostics import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestUndeclaredDiagnostics(t *testing.T) { @@ -45,7 +45,7 @@ func _() int { // 'x' is undeclared, but still necessary. env.OpenFile("a/a.go") env.Await(env.DiagnosticAtRegexp("a/a.go", "x")) - diags := env.DiagnosticsFor("a/a.go") + diags := env.Awaiter.DiagnosticsFor("a/a.go") if got := len(diags.Diagnostics); got != 1 { t.Errorf("len(Diagnostics) = %d, want 1", got) } @@ -56,7 +56,7 @@ func _() int { // 'y = y' is pointless, and should be detected as unnecessary. env.OpenFile("b/b.go") env.Await(env.DiagnosticAtRegexp("b/b.go", "y = y")) - diags = env.DiagnosticsFor("b/b.go") + diags = env.Awaiter.DiagnosticsFor("b/b.go") if got := len(diags.Diagnostics); got != 1 { t.Errorf("len(Diagnostics) = %d, want 1", got) } diff --git a/gopls/internal/regtest/inlayhints/inlayhints_test.go b/gopls/internal/regtest/inlayhints/inlayhints_test.go new file mode 100644 index 00000000000..4c8e75707e7 --- /dev/null +++ b/gopls/internal/regtest/inlayhints/inlayhints_test.go @@ -0,0 +1,71 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package inlayhint + +import ( + "testing" + + "golang.org/x/tools/gopls/internal/hooks" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/testenv" +) + +func TestMain(m *testing.M) { + bug.PanicOnBugs = true + Main(m, hooks.Options) +} + +func TestEnablingInlayHints(t *testing.T) { + testenv.NeedsGo1Point(t, 14) // Test fails on 1.13. + const workspace = ` +-- go.mod -- +module inlayHint.test +go 1.12 +-- lib.go -- +package lib +type Number int +const ( + Zero Number = iota + One + Two +) +` + tests := []struct { + label string + enabled map[string]bool + wantInlayHint bool + }{ + { + label: "default", + wantInlayHint: false, + }, + { + label: "enable const", + enabled: map[string]bool{source.ConstantValues: true}, + wantInlayHint: true, + }, + { + label: "enable parameter names", + enabled: map[string]bool{source.ParameterNames: true}, + wantInlayHint: false, + }, + } + for _, test := range tests { + t.Run(test.label, func(t *testing.T) { + WithOptions( + Settings{ + "hints": test.enabled, + }, + ).Run(t, workspace, func(t *testing.T, env *Env) { + env.OpenFile("lib.go") + lens := env.InlayHints("lib.go") + if gotInlayHint := len(lens) > 0; gotInlayHint != test.wantInlayHint { + t.Errorf("got inlayHint: %t, want %t", gotInlayHint, test.wantInlayHint) + } + }) + }) + } +} diff --git a/gopls/internal/regtest/misc/call_hierarchy_test.go b/gopls/internal/regtest/misc/call_hierarchy_test.go index 9d98896ce2e..ece05b0e614 100644 --- a/gopls/internal/regtest/misc/call_hierarchy_test.go +++ b/gopls/internal/regtest/misc/call_hierarchy_test.go @@ -6,8 +6,8 @@ package misc import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // Test for golang/go#49125 diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index d9cce96a43e..5bb2c8620a0 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -7,9 +7,8 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/fake" "golang.org/x/tools/internal/testenv" ) @@ -38,14 +37,13 @@ var FooErr = errors.New("foo") env.OpenFile("a/a.go") env.Await( env.DoneWithOpen(), - NoDiagnostics("a/a.go"), + EmptyDiagnostics("a/a.go"), ) - cfg := &fake.EditorConfig{} - *cfg = env.Editor.Config + cfg := env.Editor.Config() cfg.Settings = map[string]interface{}{ "staticcheck": true, } - env.ChangeConfiguration(t, cfg) + env.ChangeConfiguration(cfg) env.Await( DiagnosticAt("a/a.go", 5, 4), ) @@ -70,11 +68,48 @@ import "errors" var FooErr = errors.New("foo") ` - WithOptions(EditorConfig{ - Settings: map[string]interface{}{ - "staticcheck": true, + WithOptions( + Settings{"staticcheck": true}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + ShownMessage("staticcheck is not supported"), + ), + ) + }) +} + +func TestGofumptWarning(t *testing.T) { + testenv.SkipAfterGo1Point(t, 17) + + WithOptions( + Settings{"gofumpt": true}, + ).Run(t, "", func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + ShownMessage("gofumpt is not supported"), + ), + ) + }) +} + +func TestDeprecatedSettings(t *testing.T) { + WithOptions( + Settings{ + "experimentalUseInvalidMetadata": true, + "experimentalWatchedFileDelay": "1s", + "experimentalWorkspaceModule": true, }, - }).Run(t, files, func(t *testing.T, env *Env) { - env.Await(ShownMessage("staticcheck is not supported")) + ).Run(t, "", func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + ShownMessage("experimentalWorkspaceModule"), + ShownMessage("experimentalUseInvalidMetadata"), + ShownMessage("experimentalWatchedFileDelay"), + ), + ) }) } diff --git a/gopls/internal/regtest/misc/debugserver_test.go b/gopls/internal/regtest/misc/debugserver_test.go index c0df87070c0..519f7944790 100644 --- a/gopls/internal/regtest/misc/debugserver_test.go +++ b/gopls/internal/regtest/misc/debugserver_test.go @@ -8,10 +8,10 @@ import ( "net/http" "testing" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestStartDebugging(t *testing.T) { diff --git a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go index 2f5a54820d0..a5030d71fb3 100644 --- a/gopls/internal/regtest/misc/definition_test.go +++ b/gopls/internal/regtest/misc/definition_test.go @@ -9,12 +9,12 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/fake" ) const internalDefinition = ` @@ -87,7 +87,6 @@ func TestUnexportedStdlib_Issue40809(t *testing.T) { Run(t, stdlibDefinition, func(t *testing.T, env *Env) { env.OpenFile("main.go") name, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `fmt.(Printf)`)) - env.OpenFile(name) pos := env.RegexpSearch(name, `:=\s*(newPrinter)\(\)`) @@ -133,7 +132,7 @@ func main() { } want := "```go\nfunc (error).Error() string\n```" if content.Value != want { - t.Fatalf("hover failed:\n%s", tests.Diff(t, want, content.Value)) + t.Fatalf("hover failed:\n%s", compare.Text(want, content.Value)) } }) } @@ -162,9 +161,7 @@ func main() {} } { t.Run(tt.importShortcut, func(t *testing.T) { WithOptions( - EditorConfig{ - ImportShortcut: tt.importShortcut, - }, + Settings{"importShortcut": tt.importShortcut}, ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") file, pos := env.GoToDefinition("main.go", env.RegexpSearch("main.go", `"fmt"`)) diff --git a/gopls/internal/regtest/misc/embed_test.go b/gopls/internal/regtest/misc/embed_test.go index 2e66d7866ca..3730e5ab004 100644 --- a/gopls/internal/regtest/misc/embed_test.go +++ b/gopls/internal/regtest/misc/embed_test.go @@ -6,7 +6,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/extract_test.go b/gopls/internal/regtest/misc/extract_test.go new file mode 100644 index 00000000000..45ddb5408a5 --- /dev/null +++ b/gopls/internal/regtest/misc/extract_test.go @@ -0,0 +1,69 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package misc + +import ( + "testing" + + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + + "golang.org/x/tools/gopls/internal/lsp/protocol" +) + +func TestExtractFunction(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- main.go -- +package main + +func Foo() int { + a := 5 + return a +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + + start := env.RegexpSearch("main.go", "a := 5").ToProtocolPosition() + end := env.RegexpSearch("main.go", "return a").ToProtocolPosition() + + actions, err := env.Editor.CodeAction(env.Ctx, "main.go", &protocol.Range{Start: start, End: end}, nil) + if err != nil { + t.Fatal(err) + } + + // Find the extract function code action. + var extractFunc *protocol.CodeAction + for _, action := range actions { + if action.Kind == protocol.RefactorExtract && action.Title == "Extract function" { + extractFunc = &action + break + } + } + if extractFunc == nil { + t.Fatal("could not find extract function action") + } + + env.ApplyCodeAction(*extractFunc) + want := `package main + +func Foo() int { + a := newFunction() + return a +} + +func newFunction() int { + a := 5 + return a +} +` + if got := env.Editor.BufferText("main.go"); got != want { + t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) + } + }) +} diff --git a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go index 23fccfd628d..9ec5fb5916f 100644 --- a/gopls/internal/regtest/misc/failures_test.go +++ b/gopls/internal/regtest/misc/failures_test.go @@ -7,7 +7,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) // This test passes (TestHoverOnError in definition_test.go) without @@ -29,7 +29,7 @@ func main() { var err error err.Error() }` - WithOptions(SkipLogs()).Run(t, mod, func(t *testing.T, env *Env) { + Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error")) // without the //line comment content would be non-nil @@ -39,9 +39,12 @@ func main() { }) } -// badPackageDup contains a duplicate definition of the 'a' const. -// this is from diagnostics_test.go, -const badPackageDup = ` +// This test demonstrates a case where gopls is confused by line directives, +// and fails to surface type checking errors. +func TestFailingDiagnosticClearingOnEdit(t *testing.T) { + // badPackageDup contains a duplicate definition of the 'a' const. + // this is from diagnostics_test.go, + const badPackageDup = ` -- go.mod -- module mod.com @@ -56,11 +59,17 @@ package consts const a = 2 ` -func TestFailingDiagnosticClearingOnEdit(t *testing.T) { Run(t, badPackageDup, func(t *testing.T, env *Env) { env.OpenFile("b.go") + // no diagnostics for any files, but there should be - env.Await(NoDiagnostics("a.go"), NoDiagnostics("b.go")) + env.Await( + OnceMet( + env.DoneWithOpen(), + EmptyOrNoDiagnostics("a.go"), + EmptyOrNoDiagnostics("b.go"), + ), + ) // Fix the error by editing the const name in b.go to `b`. env.RegexpReplace("b.go", "(a) = 2", "b") diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index 8318ae557da..f9283d74f72 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -7,10 +7,10 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // A basic test for fillstruct, now that it uses a command. @@ -56,7 +56,7 @@ func Foo() { } ` if got := env.Editor.BufferText("main.go"); got != want { - t.Fatalf("TestFillStruct failed:\n%s", tests.Diff(t, want, got)) + t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) } }) } diff --git a/gopls/internal/regtest/misc/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go index 75d8f622458..39d58229896 100644 --- a/gopls/internal/regtest/misc/formatting_test.go +++ b/gopls/internal/regtest/misc/formatting_test.go @@ -8,9 +8,9 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" - - "golang.org/x/tools/internal/lsp/tests" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/testenv" ) const unformattedProgram = ` @@ -37,7 +37,7 @@ func TestFormatting(t *testing.T) { got := env.Editor.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.golden") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -59,7 +59,7 @@ func f() {} got := env.Editor.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.formatted") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -83,7 +83,7 @@ func f() { fmt.Println() } got := env.Editor.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.imported") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -104,7 +104,7 @@ func f() {} got := env.Editor.BufferText("a.go") want := env.ReadWorkspaceFile("a.go.imported") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -150,7 +150,7 @@ func TestOrganizeImports(t *testing.T) { got := env.Editor.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.organized") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -162,7 +162,7 @@ func TestFormattingOnSave(t *testing.T) { got := env.Editor.BufferText("main.go") want := env.ReadWorkspaceFile("main.go.formatted") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } @@ -262,7 +262,7 @@ func main() { got := env.Editor.BufferText("main.go") got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity if tt.want != got { - t.Errorf("unexpected content after save:\n%s", tests.Diff(t, tt.want, got)) + t.Errorf("unexpected content after save:\n%s", compare.Text(tt.want, got)) } }) }) @@ -303,6 +303,7 @@ func main() { } func TestGofumptFormatting(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // Exercise some gofumpt formatting rules: // - No empty lines following an assignment operator @@ -352,10 +353,8 @@ const Bar = 42 ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "gofumpt": true, - }, + Settings{ + "gofumpt": true, }, ).Run(t, input, func(t *testing.T, env *Env) { env.OpenFile("foo.go") @@ -363,7 +362,7 @@ const Bar = 42 got := env.Editor.BufferText("foo.go") want := env.ReadWorkspaceFile("foo.go.formatted") if got != want { - t.Errorf("unexpected formatting result:\n%s", tests.Diff(t, want, got)) + t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) } }) } diff --git a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go index 1dc22d737ba..ca3461d68fe 100644 --- a/gopls/internal/regtest/misc/generate_test.go +++ b/gopls/internal/regtest/misc/generate_test.go @@ -12,12 +12,10 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestGenerateProgress(t *testing.T) { - t.Skipf("skipping flaky test: https://golang.org/issue/49901") - const generatedWorkspace = ` -- go.mod -- module fake.test diff --git a/gopls/internal/regtest/misc/highlight_test.go b/gopls/internal/regtest/misc/highlight_test.go index affbffd66f4..8e7ce525e35 100644 --- a/gopls/internal/regtest/misc/highlight_test.go +++ b/gopls/internal/regtest/misc/highlight_test.go @@ -8,9 +8,9 @@ import ( "sort" "testing" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestWorkspacePackageHighlight(t *testing.T) { diff --git a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go index 4701b075acc..bea370a21ce 100644 --- a/gopls/internal/regtest/misc/hover_test.go +++ b/gopls/internal/regtest/misc/hover_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/fake" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/fake" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) diff --git a/gopls/internal/regtest/misc/import_test.go b/gopls/internal/regtest/misc/import_test.go index d5b6bcf43f1..2e95e83a562 100644 --- a/gopls/internal/regtest/misc/import_test.go +++ b/gopls/internal/regtest/misc/import_test.go @@ -8,10 +8,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" ) func TestAddImport(t *testing.T) { @@ -51,7 +51,7 @@ func main() { }, nil) got := env.Editor.BufferText("main.go") if got != want { - t.Fatalf("gopls.add_import failed\n%s", tests.Diff(t, want, got)) + t.Fatalf("gopls.add_import failed\n%s", compare.Text(want, got)) } }) } diff --git a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go index 4ae2be6bf10..8a781bd424a 100644 --- a/gopls/internal/regtest/misc/imports_test.go +++ b/gopls/internal/regtest/misc/imports_test.go @@ -11,9 +11,9 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -153,9 +153,8 @@ var _, _ = x.X, y.Y t.Fatal(err) } defer os.RemoveAll(modcache) - editorConfig := EditorConfig{Env: map[string]string{"GOMODCACHE": modcache}} WithOptions( - editorConfig, + EnvVars{"GOMODCACHE": modcache}, ProxyFiles(proxy), ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("main.go") @@ -214,3 +213,49 @@ func TestA(t *testing.T) { ) }) } + +// Test for golang/go#52784 +func TestGoWorkImports(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + const pkg = ` +-- go.work -- +go 1.19 + +use ( + ./caller + ./mod +) +-- caller/go.mod -- +module caller.com + +go 1.18 + +require mod.com v0.0.0 + +replace mod.com => ../mod +-- caller/caller.go -- +package main + +func main() { + a.Test() +} +-- mod/go.mod -- +module mod.com + +go 1.18 +-- mod/a/a.go -- +package a + +func Test() { +} +` + Run(t, pkg, func(t *testing.T, env *Env) { + env.OpenFile("caller/caller.go") + env.Await(env.DiagnosticAtRegexp("caller/caller.go", "a.Test")) + + // Saving caller.go should trigger goimports, which should find a.Test in + // the mod.com module, thanks to the go.work file. + env.SaveBuffer("caller/caller.go") + env.Await(EmptyDiagnostics("caller/caller.go")) + }) +} diff --git a/gopls/internal/regtest/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go index e84f6377eeb..5e937f73a75 100644 --- a/gopls/internal/regtest/misc/link_test.go +++ b/gopls/internal/regtest/misc/link_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) @@ -53,8 +53,8 @@ const Hello = "Hello" env.OpenFile("main.go") env.OpenFile("go.mod") - modLink := "https://pkg.go.dev/mod/import.test@v1.2.3?utm_source=gopls" - pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg?utm_source=gopls" + modLink := "https://pkg.go.dev/mod/import.test@v1.2.3" + pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg" // First, check that we get the expected links via hover and documentLink. content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello")) @@ -75,7 +75,9 @@ const Hello = "Hello" } // Then change the environment to make these links private. - env.ChangeEnv(map[string]string{"GOPRIVATE": "import.test"}) + cfg := env.Editor.Config() + cfg.Env = map[string]string{"GOPRIVATE": "import.test"} + env.ChangeConfiguration(cfg) // Finally, verify that the links are gone. content, _ = env.Hover("main.go", env.RegexpSearch("main.go", "pkg.Hello")) diff --git a/gopls/internal/regtest/misc/misc_test.go b/gopls/internal/regtest/misc/misc_test.go index c553bdb3780..12aea697c15 100644 --- a/gopls/internal/regtest/misc/misc_test.go +++ b/gopls/internal/regtest/misc/misc_test.go @@ -8,8 +8,8 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" ) func TestMain(m *testing.M) { diff --git a/gopls/internal/regtest/misc/multiple_adhoc_test.go b/gopls/internal/regtest/misc/multiple_adhoc_test.go index 5f803e4e385..400e7843483 100644 --- a/gopls/internal/regtest/misc/multiple_adhoc_test.go +++ b/gopls/internal/regtest/misc/multiple_adhoc_test.go @@ -7,7 +7,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestMultipleAdHocPackages(t *testing.T) { diff --git a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go index 768251680f9..2cd4359d313 100644 --- a/gopls/internal/regtest/misc/references_test.go +++ b/gopls/internal/regtest/misc/references_test.go @@ -5,9 +5,14 @@ package misc import ( + "fmt" + "sort" + "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestStdlibReferences(t *testing.T) { @@ -81,3 +86,205 @@ func _() { } }) } + +func TestPackageReferences(t *testing.T) { + tests := []struct { + packageName string + wantRefCount int + wantFiles []string + }{ + { + "lib1", + 3, + []string{ + "main.go", + "lib1/a.go", + "lib1/b.go", + }, + }, + { + "lib2", + 2, + []string{ + "main.go", + "lib2/a.go", + }, + }, + } + + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib1/a.go -- +package lib1 + +const A = 1 + +-- lib1/b.go -- +package lib1 + +const B = 1 + +-- lib2/a.go -- +package lib2 + +const C = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib1" + "mod.com/lib2" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + for _, test := range tests { + f := fmt.Sprintf("%s/a.go", test.packageName) + env.OpenFile(f) + pos := env.RegexpSearch(f, test.packageName) + refs := env.References(fmt.Sprintf("%s/a.go", test.packageName), pos) + if len(refs) != test.wantRefCount { + t.Fatalf("got %v reference(s), want %d", len(refs), test.wantRefCount) + } + var refURIs []string + for _, ref := range refs { + refURIs = append(refURIs, string(ref.URI)) + } + for _, base := range test.wantFiles { + hasBase := false + for _, ref := range refURIs { + if strings.HasSuffix(ref, base) { + hasBase = true + break + } + } + if !hasBase { + t.Fatalf("got [%v], want reference ends with \"%v\"", strings.Join(refURIs, ","), base) + } + } + } + }) +} + +// Test for golang/go#43144. +// +// Verify that we search for references and implementations in intermediate +// test variants. +func TestReferencesInTestVariants(t *testing.T) { + const files = ` +-- go.mod -- +module foo.mod + +go 1.12 +-- foo/foo.go -- +package foo + +import "foo.mod/bar" + +const Foo = 42 + +type T int +type Interface interface{ M() } + +func _() { + _ = bar.Blah +} + +-- bar/bar.go -- +package bar + +var Blah = 123 + +-- bar/bar_test.go -- +package bar + +type Mer struct{} +func (Mer) M() {} + +func TestBar() { + _ = Blah +} +-- bar/bar_x_test.go -- +package bar_test + +import ( + "foo.mod/bar" + "foo.mod/foo" +) + +type Mer struct{} +func (Mer) M() {} + +func _() { + _ = bar.Blah + _ = foo.Foo +} +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + + // Helper to map locations relative file paths. + fileLocations := func(locs []protocol.Location) []string { + var got []string + for _, loc := range locs { + got = append(got, env.Sandbox.Workdir.URIToPath(loc.URI)) + } + sort.Strings(got) + return got + } + + refTests := []struct { + re string + wantRefs []string + }{ + // Blah is referenced: + // - inside the foo.mod/bar (ordinary) package + // - inside the foo.mod/bar [foo.mod/bar.test] test variant package + // - from the foo.mod/bar_test [foo.mod/bar.test] x_test package + // - from the foo.mod/foo package + {"Blah", []string{"bar/bar.go", "bar/bar_test.go", "bar/bar_x_test.go", "foo/foo.go"}}, + + // Foo is referenced in bar_x_test.go via the intermediate test variant + // foo.mod/foo [foo.mod/bar.test]. + {"Foo", []string{"bar/bar_x_test.go", "foo/foo.go"}}, + } + + for _, test := range refTests { + pos := env.RegexpSearch("foo/foo.go", test.re) + refs := env.References("foo/foo.go", pos) + + got := fileLocations(refs) + if diff := cmp.Diff(test.wantRefs, got); diff != "" { + t.Errorf("References(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) + } + } + + implTests := []struct { + re string + wantImpls []string + }{ + // Interface is implemented both in foo.mod/bar [foo.mod/bar.test] (which + // doesn't import foo), and in foo.mod/bar_test [foo.mod/bar.test], which + // imports the test variant of foo. + {"Interface", []string{"bar/bar_test.go", "bar/bar_x_test.go"}}, + } + + for _, test := range implTests { + pos := env.RegexpSearch("foo/foo.go", test.re) + refs := env.Implementations("foo/foo.go", pos) + + got := fileLocations(refs) + if diff := cmp.Diff(test.wantImpls, got); diff != "" { + t.Errorf("Implementations(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) + } + } + }) +} diff --git a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go index 121b70725b4..a9fd920317f 100644 --- a/gopls/internal/regtest/misc/rename_test.go +++ b/gopls/internal/regtest/misc/rename_test.go @@ -8,9 +8,333 @@ import ( "strings" "testing" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/testenv" ) +func TestPrepareRenameMainPackage(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- main.go -- +package main + +import ( + "fmt" +) + +func main() { + fmt.Println(1) +} +` + const wantErr = "can't rename package \"main\"" + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + pos := env.RegexpSearch("main.go", `main`) + tdpp := protocol.TextDocumentPositionParams{ + TextDocument: env.Editor.TextDocumentIdentifier("main.go"), + Position: pos.ToProtocolPosition(), + } + params := &protocol.PrepareRenameParams{ + TextDocumentPositionParams: tdpp, + } + _, err := env.Editor.Server.PrepareRename(env.Ctx, params) + if err == nil { + t.Errorf("missing can't rename package main error from PrepareRename") + } + + if err.Error() != wantErr { + t.Errorf("got %v, want %v", err.Error(), wantErr) + } + }) +} + +// Test case for golang/go#56227 +func TestRenameWithUnsafeSlice(t *testing.T) { + testenv.NeedsGo1Point(t, 17) // unsafe.Slice was added in Go 1.17 + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- p.go -- +package p + +import "unsafe" + +type T struct{} + +func (T) M() {} + +func _() { + x := [3]int{1, 2, 3} + ptr := unsafe.Pointer(&x) + _ = unsafe.Slice((*int)(ptr), 3) +} +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("p.go") + env.Rename("p.go", env.RegexpSearch("p.go", "M"), "N") // must not panic + }) +} + +func TestPrepareRenameWithNoPackageDeclaration(t *testing.T) { + testenv.NeedsGo1Point(t, 15) + const files = ` +go 1.14 +-- lib/a.go -- +import "fmt" + +const A = 1 + +func bar() { + fmt.Println("Bar") +} + +-- main.go -- +package main + +import "fmt" + +func main() { + fmt.Println("Hello") +} +` + const wantErr = "no object found" + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "fmt") + + err := env.Editor.Rename(env.Ctx, "lib/a.go", pos, "fmt1") + if err == nil { + t.Errorf("missing no object found from Rename") + } + + if err.Error() != wantErr { + t.Errorf("got %v, want %v", err.Error(), wantErr) + } + }) +} + +func TestPrepareRenameFailWithUnknownModule(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +go 1.14 +-- lib/a.go -- +package lib + +const A = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" +) + +func main() { + println("Hello") +} +` + const wantErr = "can't rename package: missing module information for package" + Run(t, files, func(t *testing.T, env *Env) { + pos := env.RegexpSearch("lib/a.go", "lib") + tdpp := protocol.TextDocumentPositionParams{ + TextDocument: env.Editor.TextDocumentIdentifier("lib/a.go"), + Position: pos.ToProtocolPosition(), + } + params := &protocol.PrepareRenameParams{ + TextDocumentPositionParams: tdpp, + } + _, err := env.Editor.Server.PrepareRename(env.Ctx, params) + if err == nil || !strings.Contains(err.Error(), wantErr) { + t.Errorf("missing cannot rename packages with unknown module from PrepareRename") + } + }) +} + +func TestRenamePackageWithConflicts(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package nested + +const B = 1 + +-- lib/x/a.go -- +package nested1 + +const C = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + "mod.com/lib/nested" + nested1 "mod.com/lib/x" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", `nested2 "mod.com/nested"`) + env.RegexpSearch("main.go", "mod.com/nested/nested") + env.RegexpSearch("main.go", `nested1 "mod.com/nested/x"`) + }) +} + +func TestRenamePackageWithAlias(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package nested + +const B = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + lib1 "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) + }) +} + +func TestRenamePackageWithDifferentDirectoryPath(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package foo + +const B = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + foo "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `foo "mod.com/nested/nested"`) + }) +} + +func TestRenamePackage(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/b.go -- +package lib + +const B = 1 + +-- lib/nested/a.go -- +package nested + +const C = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "lib1") + + // Check if the new package name exists. + env.RegexpSearch("lib1/a.go", "package lib1") + env.RegexpSearch("lib1/b.go", "package lib1") + env.RegexpSearch("main.go", "mod.com/lib1") + env.RegexpSearch("main.go", "mod.com/lib1/nested") + }) +} + // Test for golang/go#47564. func TestRenameInTestVariant(t *testing.T) { const files = ` @@ -56,3 +380,505 @@ func main() { } }) } + +// This is a test that rename operation initiated by the editor function as expected. +func TestRenameFileFromEditor(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.16 +-- a/a.go -- +package a + +const X = 1 +-- a/x.go -- +package a + +const X = 2 +-- b/b.go -- +package b +` + + Run(t, files, func(t *testing.T, env *Env) { + // Rename files and verify that diagnostics are affected accordingly. + + // Initially, we should have diagnostics on both X's, for their duplicate declaration. + env.Await( + OnceMet( + InitialWorkspaceLoad, + env.DiagnosticAtRegexp("a/a.go", "X"), + env.DiagnosticAtRegexp("a/x.go", "X"), + ), + ) + + // Moving x.go should make the diagnostic go away. + env.RenameFile("a/x.go", "b/x.go") + env.Await( + OnceMet( + env.DoneWithChangeWatchedFiles(), + EmptyDiagnostics("a/a.go"), // no more duplicate declarations + env.DiagnosticAtRegexp("b/b.go", "package"), // as package names mismatch + ), + ) + + // Renaming should also work on open buffers. + env.OpenFile("b/x.go") + + // Moving x.go back to a/ should cause the diagnostics to reappear. + env.RenameFile("b/x.go", "a/x.go") + // TODO(rfindley): enable using a OnceMet precondition here. We can't + // currently do this because DidClose, DidOpen and DidChangeWatchedFiles + // are sent, and it is not easy to use all as a precondition. + env.Await( + env.DiagnosticAtRegexp("a/a.go", "X"), + env.DiagnosticAtRegexp("a/x.go", "X"), + ) + + // Renaming the entire directory should move both the open and closed file. + env.RenameFile("a", "x") + env.Await( + env.DiagnosticAtRegexp("x/a.go", "X"), + env.DiagnosticAtRegexp("x/x.go", "X"), + ) + + // As a sanity check, verify that x/x.go is open. + if text := env.Editor.BufferText("x/x.go"); text == "" { + t.Fatal("got empty buffer for x/x.go") + } + }) +} + +func TestRenamePackage_Tests(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/b.go -- +package lib + +const B = 1 + +-- lib/a_test.go -- +package lib_test + +import ( + "mod.com/lib" + "fmt +) + +const C = 1 + +-- lib/b_test.go -- +package lib + +import ( + "fmt +) + +const D = 1 + +-- lib/nested/a.go -- +package nested + +const D = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "lib1") + + // Check if the new package name exists. + env.RegexpSearch("lib1/a.go", "package lib1") + env.RegexpSearch("lib1/b.go", "package lib1") + env.RegexpSearch("main.go", "mod.com/lib1") + env.RegexpSearch("main.go", "mod.com/lib1/nested") + + // Check if the test package is renamed + env.RegexpSearch("lib1/a_test.go", "package lib1_test") + env.RegexpSearch("lib1/b_test.go", "package lib1") + }) +} + +func TestRenamePackage_NestedModule(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 + +require ( + mod.com/foo/bar v0.0.0 +) + +replace mod.com/foo/bar => ./foo/bar +-- foo/foo.go -- +package foo + +import "fmt" + +func Bar() { + fmt.Println("In foo before renamed to foox.") +} + +-- foo/bar/go.mod -- +module mod.com/foo/bar + +-- foo/bar/bar.go -- +package bar + +const Msg = "Hi" + +-- main.go -- +package main + +import ( + "fmt" + "mod.com/foo/bar" + "mod.com/foo" +) + +func main() { + foo.Bar() + fmt.Println(bar.Msg) +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + pos := env.RegexpSearch("foo/foo.go", "foo") + env.Rename("foo/foo.go", pos, "foox") + + env.RegexpSearch("foox/foo.go", "package foox") + env.OpenFile("foox/bar/bar.go") + env.OpenFile("foox/bar/go.mod") + + env.RegexpSearch("main.go", "mod.com/foo/bar") + env.RegexpSearch("main.go", "mod.com/foox") + env.RegexpSearch("main.go", "foox.Bar()") + }) +} + +func TestRenamePackage_DuplicateImport(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package nested + +const B = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + lib1 "mod.com/lib" + lib2 "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `lib1 "mod.com/nested"`) + env.RegexpSearch("main.go", `lib2 "mod.com/nested/nested"`) + }) +} + +func TestRenamePackage_DuplicateBlankImport(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +const A = 1 + +-- lib/nested/a.go -- +package nested + +const B = 1 + +-- main.go -- +package main + +import ( + "mod.com/lib" + _ "mod.com/lib" + lib1 "mod.com/lib/nested" +) + +func main() { + println("Hello") +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "lib") + env.Rename("lib/a.go", pos, "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `_ "mod.com/nested"`) + env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) + }) +} + +func TestRenamePackage_TestVariant(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- foo/foo.go -- +package foo + +const Foo = 42 +-- bar/bar.go -- +package bar + +import "mod.com/foo" + +const Bar = foo.Foo +-- bar/bar_test.go -- +package bar + +import "mod.com/foo" + +const Baz = foo.Foo +-- testdata/bar/bar.go -- +package bar + +import "mod.com/foox" + +const Bar = foox.Foo +-- testdata/bar/bar_test.go -- +package bar + +import "mod.com/foox" + +const Baz = foox.Foo +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + env.Rename("foo/foo.go", env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") + + checkTestdata(t, env) + }) +} + +func TestRenamePackage_IntermediateTestVariant(t *testing.T) { + // In this test set up, we have the following import edges: + // bar_test -> baz -> foo -> bar + // bar_test -> foo -> bar + // bar_test -> bar + // + // As a consequence, bar_x_test.go is in the reverse closure of both + // `foo [bar.test]` and `baz [bar.test]`. This test confirms that we don't + // produce duplicate edits in this case. + const files = ` +-- go.mod -- +module foo.mod + +go 1.12 +-- foo/foo.go -- +package foo + +import "foo.mod/bar" + +const Foo = 42 + +const _ = bar.Bar +-- baz/baz.go -- +package baz + +import "foo.mod/foo" + +const Baz = foo.Foo +-- bar/bar.go -- +package bar + +var Bar = 123 +-- bar/bar_test.go -- +package bar + +const _ = Bar +-- bar/bar_x_test.go -- +package bar_test + +import ( + "foo.mod/bar" + "foo.mod/baz" + "foo.mod/foo" +) + +const _ = bar.Bar + baz.Baz + foo.Foo +-- testdata/foox/foo.go -- +package foox + +import "foo.mod/bar" + +const Foo = 42 + +const _ = bar.Bar +-- testdata/baz/baz.go -- +package baz + +import "foo.mod/foox" + +const Baz = foox.Foo +-- testdata/bar/bar_x_test.go -- +package bar_test + +import ( + "foo.mod/bar" + "foo.mod/baz" + "foo.mod/foox" +) + +const _ = bar.Bar + baz.Baz + foox.Foo +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + env.Rename("foo/foo.go", env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") + + checkTestdata(t, env) + }) +} + +func TestRenamePackage_Nesting(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +import "mod.com/lib/nested" + +const A = 1 + nested.B +-- lib/nested/a.go -- +package nested + +const B = 1 +-- other/other.go -- +package other + +import ( + "mod.com/lib" + "mod.com/lib/nested" +) + +const C = lib.A + nested.B +-- testdata/libx/a.go -- +package libx + +import "mod.com/libx/nested" + +const A = 1 + nested.B +-- testdata/other/other.go -- +package other + +import ( + "mod.com/libx" + "mod.com/libx/nested" +) + +const C = libx.A + nested.B +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "package (lib)") + env.Rename("lib/a.go", pos, "libx") + + checkTestdata(t, env) + }) +} + +func TestRenamePackage_InvalidName(t *testing.T) { + testenv.NeedsGo1Point(t, 17) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- lib/a.go -- +package lib + +import "mod.com/lib/nested" + +const A = 1 + nested.B +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + pos := env.RegexpSearch("lib/a.go", "package (lib)") + + for _, badName := range []string{"$$$", "lib_test"} { + if err := env.Editor.Rename(env.Ctx, "lib/a.go", pos, badName); err == nil { + t.Errorf("Rename(lib, libx) succeeded, want non-nil error") + } + } + }) +} + +// checkTestdata checks that current buffer contents match their corresponding +// expected content in the testdata directory. +func checkTestdata(t *testing.T, env *Env) { + t.Helper() + files := env.ListFiles("testdata") + if len(files) == 0 { + t.Fatal("no files in testdata directory") + } + for _, file := range files { + suffix := strings.TrimPrefix(file, "testdata/") + got := env.Editor.BufferText(suffix) + want := env.ReadWorkspaceFile(file) + if diff := compare.Text(want, got); diff != "" { + t.Errorf("Rename: unexpected buffer content for %s (-want +got):\n%s", suffix, diff) + } + } +} diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index 79507876a64..b0296ee771f 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -7,8 +7,8 @@ package misc import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestBadURICrash_VSCodeIssue1498(t *testing.T) { @@ -25,10 +25,8 @@ func main() {} ` WithOptions( - Modes(Singleton), - EditorConfig{ - AllExperiments: true, - }, + Modes(Default), + Settings{"allExperiments": true}, ).Run(t, src, func(t *testing.T, env *Env) { params := &protocol.SemanticTokensParams{} const badURI = "http://foo" diff --git a/gopls/internal/regtest/misc/settings_test.go b/gopls/internal/regtest/misc/settings_test.go index 7704c3c043e..dd4042989a8 100644 --- a/gopls/internal/regtest/misc/settings_test.go +++ b/gopls/internal/regtest/misc/settings_test.go @@ -7,7 +7,7 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestEmptyDirectoryFilters_Issue51843(t *testing.T) { @@ -24,11 +24,7 @@ func main() { ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "directoryFilters": []string{""}, - }, - }, + Settings{"directoryFilters": []string{""}}, ).Run(t, src, func(t *testing.T, env *Env) { // No need to do anything. Issue golang/go#51843 is triggered by the empty // directory filter above. diff --git a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go index 6861743ff42..e47ca2959d1 100644 --- a/gopls/internal/regtest/misc/shared_test.go +++ b/gopls/internal/regtest/misc/shared_test.go @@ -7,10 +7,13 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/fake" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) -const sharedProgram = ` +// Smoke test that simultaneous editing sessions in the same workspace works. +func TestSimultaneousEdits(t *testing.T) { + const sharedProgram = ` -- go.mod -- module mod @@ -24,20 +27,25 @@ func main() { fmt.Println("Hello World.") }` -func runShared(t *testing.T, testFunc func(env1 *Env, env2 *Env)) { - // Only run these tests in forwarded modes. - modes := DefaultModes() & (Forwarded | SeparateProcess) - WithOptions(Modes(modes)).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { + WithOptions( + Modes(DefaultModes()&(Forwarded|SeparateProcess)), + ).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { // Create a second test session connected to the same workspace and server // as the first. - env2 := NewEnv(env1.Ctx, t, env1.Sandbox, env1.Server, env1.Editor.Config, true) + awaiter := NewAwaiter(env1.Sandbox.Workdir) + editor, err := fake.NewEditor(env1.Sandbox, env1.Editor.Config()).Connect(env1.Ctx, env1.Server, awaiter.Hooks()) + if err != nil { + t.Fatal(err) + } + env2 := &Env{ + T: t, + Ctx: env1.Ctx, + Sandbox: env1.Sandbox, + Server: env1.Server, + Editor: editor, + Awaiter: awaiter, + } env2.Await(InitialWorkspaceLoad) - testFunc(env1, env2) - }) -} - -func TestSimultaneousEdits(t *testing.T) { - runShared(t, func(env1 *Env, env2 *Env) { // In editor #1, break fmt.Println as before. env1.OpenFile("main.go") env1.RegexpReplace("main.go", "Printl(n)", "") @@ -48,17 +56,19 @@ func TestSimultaneousEdits(t *testing.T) { // Now check that we got different diagnostics in each environment. env1.Await(env1.DiagnosticAtRegexp("main.go", "Printl")) env2.Await(env2.DiagnosticAtRegexp("main.go", "$")) - }) -} -func TestShutdown(t *testing.T) { - runShared(t, func(env1 *Env, env2 *Env) { - if err := env1.Editor.Close(env1.Ctx); err != nil { - t.Errorf("closing first editor: %v", err) + // Now close editor #2, and verify that operation in editor #1 is + // unaffected. + if err := env2.Editor.Close(env2.Ctx); err != nil { + t.Errorf("closing second editor: %v", err) } - // Now make an edit in editor #2 to trigger diagnostics. - env2.OpenFile("main.go") - env2.RegexpReplace("main.go", "\\)\n(})", "") - env2.Await(env2.DiagnosticAtRegexp("main.go", "$")) + + env1.RegexpReplace("main.go", "Printl", "Println") + env1.Await( + OnceMet( + env1.DoneWithChange(), + EmptyDiagnostics("main.go"), + ), + ) }) } diff --git a/gopls/internal/regtest/misc/staticcheck_test.go b/gopls/internal/regtest/misc/staticcheck_test.go index 94bb39903a5..f2ba3ccd8a7 100644 --- a/gopls/internal/regtest/misc/staticcheck_test.go +++ b/gopls/internal/regtest/misc/staticcheck_test.go @@ -9,7 +9,7 @@ import ( "golang.org/x/tools/internal/testenv" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestStaticcheckGenerics(t *testing.T) { @@ -60,11 +60,9 @@ func testGenerics[P *T, T any](p P) { var FooErr error = errors.New("foo") ` - WithOptions(EditorConfig{ - Settings: map[string]interface{}{ - "staticcheck": true, - }, - }).Run(t, files, func(t *testing.T, env *Env) { + WithOptions( + Settings{"staticcheck": true}, + ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.Await( env.DiagnosticAtRegexpFromSource("a/a.go", "sort.Slice", "sortslice"), @@ -76,3 +74,40 @@ var FooErr error = errors.New("foo") ) }) } + +// Test for golang/go#56270: an analysis with related info should not panic if +// analysis.RelatedInformation.End is not set. +func TestStaticcheckRelatedInfo(t *testing.T) { + testenv.NeedsGo1Point(t, 17) // staticcheck is only supported at Go 1.17+ + const files = ` +-- go.mod -- +module mod.test + +go 1.18 +-- p.go -- +package p + +import ( + "fmt" +) + +func Foo(enabled interface{}) { + if enabled, ok := enabled.(bool); ok { + } else { + _ = fmt.Sprintf("invalid type %T", enabled) // enabled is always bool here + } +} +` + + WithOptions( + Settings{"staticcheck": true}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("p.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexpFromSource("p.go", ", (enabled)", "SA9008"), + ), + ) + }) +} diff --git a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go index 324a8006fa7..cec33cad173 100644 --- a/gopls/internal/regtest/misc/vendor_test.go +++ b/gopls/internal/regtest/misc/vendor_test.go @@ -7,9 +7,9 @@ package misc import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -27,16 +27,6 @@ var Goodbye error func TestInconsistentVendoring(t *testing.T) { testenv.NeedsGo1Point(t, 14) - // TODO(golang/go#49646): delete this comment once this test is stable. - // - // In golang/go#49646, this test is reported as flaky on Windows. We believe - // this is due to file contention from go mod vendor that should be resolved. - // If this test proves to still be flaky, skip it. - // - // if runtime.GOOS == "windows" { - // t.Skipf("skipping test due to flakiness on Windows: https://golang.org/issue/49646") - // } - const pkgThatUsesVendoring = ` -- go.mod -- module mod.com @@ -59,7 +49,7 @@ func _() { } ` WithOptions( - Modes(Singleton), + Modes(Default), ProxyFiles(basicProxy), ).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) { env.OpenFile("a/a1.go") diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go index 94fde715c77..2afef9df78a 100644 --- a/gopls/internal/regtest/misc/vuln_test.go +++ b/gopls/internal/regtest/misc/vuln_test.go @@ -2,14 +2,23 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.18 +// +build go1.18 + package misc import ( + "context" + "strings" "testing" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/gopls/internal/vulncheck" + "golang.org/x/tools/gopls/internal/vulncheck/vulntest" + "golang.org/x/tools/internal/testenv" ) func TestRunVulncheckExpError(t *testing.T) { @@ -23,7 +32,7 @@ package foo ` Run(t, files, func(t *testing.T, env *Env) { cmd, err := command.NewRunVulncheckExpCommand("Run Vulncheck Exp", command.VulncheckArgs{ - Dir: "/invalid/file/url", // invalid arg + URI: "/invalid/file/url", // invalid arg }) if err != nil { t.Fatal(err) @@ -41,3 +50,421 @@ package foo } }) } + +const vulnsData = ` +-- GO-2022-01.yaml -- +modules: + - module: golang.org/amod + versions: + - introduced: 1.0.0 + - fixed: 1.0.4 + - introduced: 1.1.2 + packages: + - package: golang.org/amod/avuln + symbols: + - VulnData.Vuln1 + - VulnData.Vuln2 +description: > + vuln in amod +references: + - href: pkg.go.dev/vuln/GO-2022-01 +-- GO-2022-03.yaml -- +modules: + - module: golang.org/amod + versions: + - introduced: 1.0.0 + - fixed: 1.0.6 + packages: + - package: golang.org/amod/avuln + symbols: + - nonExisting +description: > + unaffecting vulnerability +-- GO-2022-02.yaml -- +modules: + - module: golang.org/bmod + packages: + - package: golang.org/bmod/bvuln + symbols: + - Vuln +description: | + vuln in bmod + + This is a long description + of this vulnerability. +references: + - href: pkg.go.dev/vuln/GO-2022-03 +-- STD.yaml -- +modules: + - module: stdlib + versions: + - introduced: 1.18.0 + packages: + - package: archive/zip + symbols: + - OpenReader +references: + - href: pkg.go.dev/vuln/STD +` + +func TestRunVulncheckExpStd(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- main.go -- +package main + +import ( + "archive/zip" + "fmt" +) + +func main() { + _, err := zip.OpenReader("file.zip") // vulnerability id: STD + fmt.Println(err) +} +` + + db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + WithOptions( + EnvVars{ + // Let the analyzer read vulnerabilities data from the testdata/vulndb. + "GOVULNDB": db.URI(), + // When fetchinging stdlib package vulnerability info, + // behave as if our go version is go1.18 for this testing. + // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). + vulncheck.GoVersionForVulnTest: "go1.18", + "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. + }, + Settings{ + "codelenses": map[string]bool{ + "run_vulncheck_exp": true, + }, + }, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("go.mod") + + // Test CodeLens is present. + lenses := env.CodeLens("go.mod") + + const wantCommand = "gopls." + string(command.RunVulncheckExp) + var gotCodelens = false + var lens protocol.CodeLens + for _, l := range lenses { + if l.Command.Command == wantCommand { + gotCodelens = true + lens = l + break + } + } + if !gotCodelens { + t.Fatal("got no vulncheck codelens") + } + // Run Command included in the codelens. + env.ExecuteCommand(&protocol.ExecuteCommandParams{ + Command: lens.Command.Command, + Arguments: lens.Command.Arguments, + }, nil) + env.Await( + CompletedWork("govulncheck", 1, true), + // TODO(hyangah): once the diagnostics are published, wait for diagnostics. + ShownMessage("Found STD"), + ) + }) +} + +const workspace1 = ` +-- go.mod -- +module golang.org/entry + +go 1.18 + +require golang.org/cmod v1.1.3 + +require ( + golang.org/amod v1.0.0 // indirect + golang.org/bmod v0.5.0 // indirect +) +-- go.sum -- +golang.org/amod v1.0.0 h1:EUQOI2m5NhQZijXZf8WimSnnWubaFNrrKUH/PopTN8k= +golang.org/amod v1.0.0/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E= +golang.org/bmod v0.5.0 h1:0kt1EI53298Ta9w4RPEAzNUQjtDoHUA6cc0c7Rwxhlk= +golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c= +golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ= +golang.org/cmod v1.1.3/go.mod h1:eCR8dnmvLYQomdeAZRCPgS5JJihXtqOQrpEkNj5feQA= +-- x/x.go -- +package x + +import ( + "golang.org/cmod/c" + "golang.org/entry/y" +) + +func X() { + c.C1().Vuln1() // vuln use: X -> Vuln1 +} + +func CallY() { + y.Y() // vuln use: CallY -> y.Y -> bvuln.Vuln +} + +-- y/y.go -- +package y + +import "golang.org/cmod/c" + +func Y() { + c.C2()() // vuln use: Y -> bvuln.Vuln +} +` + +const proxy1 = ` +-- golang.org/cmod@v1.1.3/go.mod -- +module golang.org/cmod + +go 1.12 +-- golang.org/cmod@v1.1.3/c/c.go -- +package c + +import ( + "golang.org/amod/avuln" + "golang.org/bmod/bvuln" +) + +type I interface { + Vuln1() +} + +func C1() I { + v := avuln.VulnData{} + v.Vuln2() // vuln use + return v +} + +func C2() func() { + return bvuln.Vuln +} +-- golang.org/amod@v1.0.0/go.mod -- +module golang.org/amod + +go 1.14 +-- golang.org/amod@v1.0.0/avuln/avuln.go -- +package avuln + +type VulnData struct {} +func (v VulnData) Vuln1() {} +func (v VulnData) Vuln2() {} +-- golang.org/amod@v1.0.4/go.mod -- +module golang.org/amod + +go 1.14 +-- golang.org/amod@v1.0.4/avuln/avuln.go -- +package avuln + +type VulnData struct {} +func (v VulnData) Vuln1() {} +func (v VulnData) Vuln2() {} + +-- golang.org/bmod@v0.5.0/go.mod -- +module golang.org/bmod + +go 1.14 +-- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- +package bvuln + +func Vuln() { + // something evil +} +` + +func TestRunVulncheckExp(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + + db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + WithOptions( + ProxyFiles(proxy1), + EnvVars{ + // Let the analyzer read vulnerabilities data from the testdata/vulndb. + "GOVULNDB": db.URI(), + // When fetching stdlib package vulnerability info, + // behave as if our go version is go1.18 for this testing. + // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). + vulncheck.GoVersionForVulnTest: "go1.18", + "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. + }, + Settings{ + "codelenses": map[string]bool{ + "run_vulncheck_exp": true, + }, + }, + ).Run(t, workspace1, func(t *testing.T, env *Env) { + env.OpenFile("go.mod") + + env.ExecuteCodeLensCommand("go.mod", command.RunVulncheckExp) + d := &protocol.PublishDiagnosticsParams{} + env.Await( + CompletedWork("govulncheck", 1, true), + ShownMessage("Found"), + OnceMet( + env.DiagnosticAtRegexp("go.mod", `golang.org/amod`), + ReadDiagnostics("go.mod", d), + ), + ) + + type diagnostic struct { + msg string + severity protocol.DiagnosticSeverity + // codeActions is a list titles of code actions that we get with this + // diagnostics as the context. + codeActions []string + } + // wantDiagnostics maps a module path in the require + // section of a go.mod to diagnostics that will be returned + // when running vulncheck. + wantDiagnostics := map[string]struct { + // applyAction is the title of the code action to run for this module. + // If empty, no code actions will be executed. + applyAction string + // diagnostics is the list of diagnostics we expect at the require line for + // the module path. + diagnostics []diagnostic + // codeActions is a list titles of code actions that we get with context + // diagnostics. + codeActions []string + // hover message is the list of expected hover message parts for this go.mod require line. + // all parts must appear in the hover message. + hover []string + }{ + "golang.org/amod": { + applyAction: "Upgrade to v1.0.4", + diagnostics: []diagnostic{ + { + msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", + severity: protocol.SeverityWarning, + codeActions: []string{ + "Upgrade to latest", + "Upgrade to v1.0.4", + }, + }, + }, + codeActions: []string{ + "Upgrade to latest", + "Upgrade to v1.0.4", + }, + hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, + }, + "golang.org/bmod": { + diagnostics: []diagnostic{ + { + msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.", + severity: protocol.SeverityWarning, + }, + }, + hover: []string{"GO-2022-02", "This is a long description of this vulnerability.", "No fix is available."}, + }, + } + + for mod, want := range wantDiagnostics { + pos := env.RegexpSearch("go.mod", mod) + var modPathDiagnostics []protocol.Diagnostic + for _, w := range want.diagnostics { + // Find the diagnostics at pos. + var diag *protocol.Diagnostic + for _, g := range d.Diagnostics { + g := g + if g.Range.Start == pos.ToProtocolPosition() && w.msg == g.Message { + modPathDiagnostics = append(modPathDiagnostics, g) + diag = &g + break + } + } + if diag == nil { + t.Errorf("no diagnostic at %q matching %q found\n", mod, w.msg) + continue + } + if diag.Severity != w.severity { + t.Errorf("incorrect severity for %q, expected %s got %s\n", w.msg, w.severity, diag.Severity) + } + + gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag}) + if !sameCodeActions(gotActions, w.codeActions) { + t.Errorf("code actions for %q do not match, expected %v, got %v\n", w.msg, w.codeActions, gotActions) + continue + } + + // Check that useful info is supplemented as hover. + if len(want.hover) > 0 { + hover, _ := env.Hover("go.mod", pos) + for _, part := range want.hover { + if !strings.Contains(hover.Value, part) { + t.Errorf("hover contents for %q do not match, expected %v, got %v\n", w.msg, strings.Join(want.hover, ","), hover.Value) + break + } + } + } + } + + // Check that the actions we get when including all diagnostics at a location return the same result + gotActions := env.CodeAction("go.mod", modPathDiagnostics) + if !sameCodeActions(gotActions, want.codeActions) { + t.Errorf("code actions for %q do not match, expected %v, got %v\n", mod, want.codeActions, gotActions) + continue + } + + // Apply the code action matching applyAction. + if want.applyAction == "" { + continue + } + for _, action := range gotActions { + if action.Title == want.applyAction { + env.ApplyCodeAction(action) + break + } + } + + } + + env.Await(env.DoneWithChangeWatchedFiles()) + wantGoMod := `module golang.org/entry + +go 1.18 + +require golang.org/cmod v1.1.3 + +require ( + golang.org/amod v1.0.4 // indirect + golang.org/bmod v0.5.0 // indirect +) +` + if got := env.Editor.BufferText("go.mod"); got != wantGoMod { + t.Fatalf("go.mod vulncheck fix failed:\n%s", compare.Text(wantGoMod, got)) + } + }) +} + +func sameCodeActions(gotActions []protocol.CodeAction, want []string) bool { + gotTitles := make([]string, len(gotActions)) + for i, ca := range gotActions { + gotTitles[i] = ca.Title + } + if len(gotTitles) != len(want) { + return false + } + for i := range want { + if gotTitles[i] != want[i] { + return false + } + } + return true +} diff --git a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go index a21d47312dd..d1fc8646cef 100644 --- a/gopls/internal/regtest/misc/workspace_symbol_test.go +++ b/gopls/internal/regtest/misc/workspace_symbol_test.go @@ -7,9 +7,9 @@ package misc import ( "testing" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/internal/testenv" ) @@ -26,13 +26,14 @@ go 1.17 package p const C1 = "a.go" --- ignore.go -- +-- exclude.go -- -// +build ignore +//go:build exclude +// +build exclude -package ignore +package exclude -const C2 = "ignore.go" +const C2 = "exclude.go" ` Run(t, files, func(t *testing.T, env *Env) { @@ -44,7 +45,7 @@ const C2 = "ignore.go" // Opening up an ignored file will result in an overlay with missing // metadata, but this shouldn't break workspace symbols requests. - env.OpenFile("ignore.go") + env.OpenFile("exclude.go") syms = env.WorkspaceSymbol("C") if got, want := len(syms), 1; got != want { t.Errorf("got %d symbols, want %d", got, want) @@ -72,9 +73,7 @@ const ( var symbolMatcher = string(source.SymbolFastFuzzy) WithOptions( - EditorConfig{ - SymbolMatcher: &symbolMatcher, - }, + Settings{"symbolMatcher": symbolMatcher}, ).Run(t, files, func(t *testing.T, env *Env) { want := []string{ "Foo", // prefer exact segment matches first @@ -105,9 +104,7 @@ const ( var symbolMatcher = string(source.SymbolFastFuzzy) WithOptions( - EditorConfig{ - SymbolMatcher: &symbolMatcher, - }, + Settings{"symbolMatcher": symbolMatcher}, ).Run(t, files, func(t *testing.T, env *Env) { compareSymbols(t, env.WorkspaceSymbol("ABC"), []string{"ABC", "AxxBxxCxx"}) compareSymbols(t, env.WorkspaceSymbol("'ABC"), []string{"ABC"}) diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go index 93d43253044..64892be5966 100644 --- a/gopls/internal/regtest/modfile/modfile_test.go +++ b/gopls/internal/regtest/modfile/modfile_test.go @@ -11,11 +11,11 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -95,10 +95,13 @@ func main() { goModContent := env.ReadWorkspaceFile("a/go.mod") env.OpenFile("a/main.go") env.Await( - env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + ), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { - t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got)) + t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) } // Save the buffer, which will format and organize imports. // Confirm that the go.mod file still does not change. @@ -107,34 +110,51 @@ func main() { env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), ) if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { - t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got)) + t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) } }) }) // Reproduce golang/go#40269 by deleting and recreating main.go. t.Run("delete main.go", func(t *testing.T) { - t.Skip("This test will be flaky until golang/go#40269 is resolved.") - runner.Run(t, untidyModule, func(t *testing.T, env *Env) { goModContent := env.ReadWorkspaceFile("a/go.mod") mainContent := env.ReadWorkspaceFile("a/main.go") env.OpenFile("a/main.go") env.SaveBuffer("a/main.go") + // Ensure that we're done processing all the changes caused by opening + // and saving above. If not, we may run into a file locking issue on + // windows. + // + // If this proves insufficient, env.RemoveWorkspaceFile can be updated to + // retry file lock errors on windows. + env.Await( + env.DoneWithOpen(), + env.DoneWithSave(), + env.DoneWithChangeWatchedFiles(), + ) env.RemoveWorkspaceFile("a/main.go") + + // TODO(rfindley): awaiting here shouldn't really be necessary. We should + // be consistent eventually. + // + // Probably this was meant to exercise a race with the change below. env.Await( env.DoneWithOpen(), env.DoneWithSave(), env.DoneWithChangeWatchedFiles(), ) - env.WriteWorkspaceFile("main.go", mainContent) + env.WriteWorkspaceFile("a/main.go", mainContent) env.Await( - env.DiagnosticAtRegexp("main.go", "\"example.com/blah\""), + OnceMet( + env.DoneWithChangeWatchedFiles(), + env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""), + ), ) - if got := env.ReadWorkspaceFile("go.mod"); got != goModContent { - t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got)) + if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { + t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) } }) }) @@ -186,7 +206,7 @@ require example.com v1.2.3 } env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{goGetDiag}) if got := env.ReadWorkspaceFile("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -236,7 +256,7 @@ require random.org v1.2.3 } env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag}) if got := env.ReadWorkspaceFile("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -292,7 +312,7 @@ require random.org v1.2.3 } env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag}) if got := env.ReadWorkspaceFile("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -339,7 +359,7 @@ require example.com v1.2.3 ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) if got := env.Editor.BufferText("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -384,7 +404,7 @@ go 1.14 ) env.ApplyQuickFixes("a/go.mod", d.Diagnostics) if got := env.Editor.BufferText("a/go.mod"); got != want { - t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) } }) } @@ -456,7 +476,7 @@ require ( ) ` if got := env.ReadWorkspaceFile("a/go.mod"); got != want { - t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", tests.Diff(t, want, got)) + t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", compare.Text(want, got)) } }) } @@ -552,7 +572,7 @@ var _ = blah.Name env.DiagnosticAtRegexpWithMessage("a/main.go", `"example.com/blah/v2"`, "cannot find module providing"), env.DiagnosticAtRegexpWithMessage("a/go.mod", `require example.com/blah/v2`, "cannot find module providing"), ) - env.ApplyQuickFixes("a/go.mod", env.DiagnosticsFor("a/go.mod").Diagnostics) + env.ApplyQuickFixes("a/go.mod", env.Awaiter.DiagnosticsFor("a/go.mod").Diagnostics) const want = `module mod.com go 1.12 @@ -565,7 +585,7 @@ require ( env.SaveBuffer("a/go.mod") env.Await(EmptyDiagnostics("a/main.go")) if got := env.Editor.BufferText("a/go.mod"); got != want { - t.Fatalf("suggested fixes failed:\n%s", tests.Diff(t, want, got)) + t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got)) } }) } @@ -576,7 +596,9 @@ func TestUnknownRevision(t *testing.T) { t.Skipf("skipping test that fails for unknown reasons on plan9; see https://go.dev/issue/50477") } - testenv.NeedsGo1Point(t, 14) + // This test fails at go1.14 and go1.15 due to differing Go command behavior. + // This was not significantly investigated. + testenv.NeedsGo1Point(t, 16) const unknown = ` -- a/go.mod -- @@ -611,7 +633,7 @@ func main() { d := protocol.PublishDiagnosticsParams{} env.Await( - OnceMet( + env.AfterChange( // Make sure the diagnostic mentions the new version -- the old diagnostic is in the same place. env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "example.com@v1.2.3"), ReadDiagnostics("a/go.mod", &d), @@ -624,8 +646,10 @@ func main() { env.ApplyCodeAction(qfs[0]) // Arbitrarily pick a single fix to apply. Applying all of them seems to cause trouble in this particular test. env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. env.Await( - EmptyDiagnostics("a/go.mod"), - env.DiagnosticAtRegexp("a/main.go", "x = "), + env.AfterChange( + EmptyDiagnostics("a/go.mod"), + env.DiagnosticAtRegexp("a/main.go", "x = "), + ), ) }) }) @@ -655,17 +679,23 @@ func main() { runner.Run(t, known, func(t *testing.T, env *Env) { env.OpenFile("a/go.mod") env.Await( - env.DiagnosticAtRegexp("a/main.go", "x = "), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", "x = "), + ), ) env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2") env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk env.Await( - env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"), + env.AfterChange( + env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"), + ), ) env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk env.Await( - env.DiagnosticAtRegexp("a/main.go", "x = "), + env.AfterChange( + env.DiagnosticAtRegexp("a/main.go", "x = "), + ), ) }) }) @@ -740,13 +770,9 @@ func main() { } ` WithOptions( - EditorConfig{ - Env: map[string]string{ - "GOFLAGS": "-mod=readonly", - }, - }, + EnvVars{"GOFLAGS": "-mod=readonly"}, ProxyFiles(proxy), - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") original := env.ReadWorkspaceFile("go.mod") @@ -755,7 +781,7 @@ func main() { ) got := env.ReadWorkspaceFile("go.mod") if got != original { - t.Fatalf("go.mod file modified:\n%s", tests.Diff(t, original, got)) + t.Fatalf("go.mod file modified:\n%s", compare.Text(original, got)) } env.RunGoCommand("get", "example.com/blah@v1.2.3") env.RunGoCommand("mod", "tidy") @@ -830,9 +856,7 @@ func main() { ` WithOptions( ProxyFiles(workspaceProxy), - EditorConfig{ - BuildFlags: []string{"-tags", "bob"}, - }, + Settings{"buildFlags": []string{"-tags", "bob"}}, ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( env.DiagnosticAtRegexp("main.go", `"example.com/blah"`), @@ -861,8 +885,6 @@ func main() {} } func TestSumUpdateFixesDiagnostics(t *testing.T) { - t.Skipf("Skipping known-flaky test; see https://go.dev/issue/51352.") - testenv.NeedsGo1Point(t, 14) const mod = ` @@ -928,7 +950,7 @@ func hello() {} // TODO(rFindley) this doesn't work in multi-module workspace mode, because // it keeps around the last parsing modfile. Update this test to also // exercise the workspace module. - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") env.Await(env.DoneWithOpen()) @@ -1012,7 +1034,7 @@ go 1.12 ` env.ApplyQuickFixes("go.mod", d.Diagnostics) if got := env.Editor.BufferText("go.mod"); got != want { - t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) } }) }) @@ -1065,7 +1087,7 @@ require random.com v1.2.3 } env.ApplyQuickFixes("go.mod", diagnostics) if got := env.Editor.BufferText("go.mod"); got != want { - t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) } }) }) @@ -1096,7 +1118,7 @@ func main() { ` WithOptions( ProxyFiles(workspaceProxy), - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("go.mod") params := &protocol.PublishDiagnosticsParams{} @@ -1111,7 +1133,7 @@ func main() { example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ` if got := env.ReadWorkspaceFile("go.sum"); got != want { - t.Fatalf("unexpected go.sum contents:\n%s", tests.Diff(t, want, got)) + t.Fatalf("unexpected go.sum contents:\n%s", compare.Text(want, got)) } }) } @@ -1165,7 +1187,7 @@ func main() { ` WithOptions( ProxyFiles(proxy), - Modes(Singleton), + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.OpenFile("main.go") d := &protocol.PublishDiagnosticsParams{} @@ -1175,8 +1197,8 @@ func main() { ) env.ApplyQuickFixes("main.go", d.Diagnostics) env.Await( - EmptyDiagnostics("main.go"), - NoDiagnostics("go.mod"), + EmptyOrNoDiagnostics("main.go"), + EmptyOrNoDiagnostics("go.mod"), ) }) } diff --git a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go index 9489e9bf7fe..91c704de0d9 100644 --- a/gopls/internal/regtest/template/template_test.go +++ b/gopls/internal/regtest/template/template_test.go @@ -9,9 +9,9 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/protocol" - . "golang.org/x/tools/internal/lsp/regtest" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" ) func TestMain(m *testing.M) { @@ -35,11 +35,9 @@ go 1.17 {{end}} ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl"}, - "semanticTokens": true, - }, + Settings{ + "templateExtensions": []string{"tmpl"}, + "semanticTokens": true, }, ).Run(t, files, func(t *testing.T, env *Env) { var p protocol.SemanticTokensParams @@ -66,16 +64,14 @@ Hello {{}} <-- missing body {{end}} ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl"}, - "semanticTokens": true, - }, + Settings{ + "templateExtensions": []string{"tmpl"}, + "semanticTokens": true, }, ).Run(t, files, func(t *testing.T, env *Env) { // TODO: can we move this diagnostic onto {{}}? env.Await(env.DiagnosticAtRegexp("hello.tmpl", "()Hello {{}}")) - d := env.DiagnosticsFor("hello.tmpl").Diagnostics // issue 50786: check for Source + d := env.Awaiter.DiagnosticsFor("hello.tmpl").Diagnostics // issue 50786: check for Source if len(d) != 1 { t.Errorf("expected 1 diagnostic, got %d", len(d)) return @@ -112,11 +108,9 @@ B {{}} <-- missing body ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl"}, - }, - DirectoryFilters: []string{"-b"}, + Settings{ + "directoryFilters": []string{"-b"}, + "templateExtensions": []string{"tmpl"}, }, ).Run(t, files, func(t *testing.T, env *Env) { env.Await( @@ -139,7 +133,7 @@ go 1.12 env.Await( OnceMet( env.DoneWithOpen(), - NoDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates. + EmptyDiagnostics("hello.tmpl"), // Don't get spurious errors for empty templates. ), ) env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") @@ -184,10 +178,8 @@ go 1.12 ` WithOptions( - EditorConfig{ - Settings: map[string]interface{}{ - "templateExtensions": []string{"tmpl", "gotmpl"}, - }, + Settings{ + "templateExtensions": []string{"tmpl", "gotmpl"}, }, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a.tmpl") diff --git a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go index e66d08ab125..1be766b8689 100644 --- a/gopls/internal/regtest/watch/watch_test.go +++ b/gopls/internal/regtest/watch/watch_test.go @@ -8,11 +8,11 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/bug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/testenv" ) @@ -139,7 +139,7 @@ func _() { }) env.Await( EmptyDiagnostics("a/a.go"), - NoDiagnostics("b/b.go"), + EmptyOrNoDiagnostics("b/b.go"), ) }) } @@ -199,14 +199,12 @@ func _() { } ` Run(t, missing, func(t *testing.T, env *Env) { - t.Skip("the initial workspace load fails and never retries") - env.Await( env.DiagnosticAtRegexp("a/a.go", "\"mod.com/c\""), ) env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) env.Await( - EmptyDiagnostics("c/c.go"), + EmptyDiagnostics("a/a.go"), ) }) } @@ -343,12 +341,12 @@ func _() { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), + EmptyOrNoDiagnostics("a/a.go"), ), ) env.WriteWorkspaceFile("b/b.go", newMethod) env.Await( - NoDiagnostics("a/a.go"), + EmptyOrNoDiagnostics("a/a.go"), ) }) }) @@ -362,9 +360,9 @@ func _() { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), + EmptyOrNoDiagnostics("a/a.go"), ), - NoDiagnostics("b/b.go"), + EmptyOrNoDiagnostics("b/b.go"), ) }) }) @@ -389,9 +387,9 @@ func _() { package a ` t.Run("close then delete", func(t *testing.T) { - WithOptions(EditorConfig{ - VerboseOutput: true, - }).Run(t, pkg, func(t *testing.T, env *Env) { + WithOptions( + Settings{"verboseOutput": true}, + ).Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("a/a_unneeded.go") env.Await( @@ -424,7 +422,7 @@ package a t.Run("delete then close", func(t *testing.T) { WithOptions( - EditorConfig{VerboseOutput: true}, + Settings{"verboseOutput": true}, ).Run(t, pkg, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.OpenFile("a/a_unneeded.go") @@ -620,11 +618,7 @@ func main() { ` WithOptions( InGOPATH(), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "auto", - }, - }, + EnvVars{"GO111MODULE": "auto"}, Modes(Experimental), // module is in a subdirectory ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/main.go") @@ -663,11 +657,7 @@ func main() { ` WithOptions( InGOPATH(), - EditorConfig{ - Env: map[string]string{ - "GO111MODULE": "auto", - }, - }, + EnvVars{"GO111MODULE": "auto"}, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("foo/main.go") env.RemoveWorkspaceFile("foo/go.mod") @@ -725,11 +715,11 @@ func TestAll(t *testing.T) { env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a.go"), + EmptyOrNoDiagnostics("a/a.go"), ), OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a_test.go"), + EmptyOrNoDiagnostics("a/a_test.go"), ), ) // Now, add a new file to the test variant and use its symbol in the @@ -757,11 +747,11 @@ func TestSomething(t *testing.T) {} env.Await( OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a_test.go"), + EmptyOrNoDiagnostics("a/a_test.go"), ), OnceMet( env.DoneWithChangeWatchedFiles(), - NoDiagnostics("a/a2_test.go"), + EmptyOrNoDiagnostics("a/a2_test.go"), ), ) }) diff --git a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go new file mode 100644 index 00000000000..f839b0a3419 --- /dev/null +++ b/gopls/internal/regtest/workspace/broken_test.go @@ -0,0 +1,254 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "strings" + "testing" + + "golang.org/x/tools/gopls/internal/lsp" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" +) + +// This file holds various tests for UX with respect to broken workspaces. +// +// TODO: consolidate other tests here. +// +// TODO: write more tests: +// - an explicit GOWORK value that doesn't exist +// - using modules and/or GOWORK inside of GOPATH? + +// Test for golang/go#53933 +func TestBrokenWorkspace_DuplicateModules(t *testing.T) { + testenv.NeedsGo1Point(t, 18) + + // This proxy module content is replaced by the workspace, but is still + // required for module resolution to function in the Go command. + const proxy = ` +-- example.com/foo@v0.0.1/go.mod -- +module example.com/foo + +go 1.12 +` + + const src = ` +-- go.work -- +go 1.18 + +use ( + ./package1 + ./package1/vendor/example.com/foo + ./package2 + ./package2/vendor/example.com/foo +) + +-- package1/go.mod -- +module mod.test + +go 1.18 + +require example.com/foo v0.0.1 +-- package1/main.go -- +package main + +import "example.com/foo" + +func main() { + _ = foo.CompleteMe +} +-- package1/vendor/example.com/foo/go.mod -- +module example.com/foo + +go 1.18 +-- package1/vendor/example.com/foo/foo.go -- +package foo + +const CompleteMe = 111 +-- package2/go.mod -- +module mod2.test + +go 1.18 + +require example.com/foo v0.0.1 +-- package2/main.go -- +package main + +import "example.com/foo" + +func main() { + _ = foo.CompleteMe +} +-- package2/vendor/example.com/foo/go.mod -- +module example.com/foo + +go 1.18 +-- package2/vendor/example.com/foo/foo.go -- +package foo + +const CompleteMe = 222 +` + + WithOptions( + ProxyFiles(proxy), + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("package1/main.go") + env.Await( + OutstandingWork(lsp.WorkspaceLoadFailure, `found module "example.com/foo" multiple times in the workspace`), + ) + + // Remove the redundant vendored copy of example.com. + env.WriteWorkspaceFile("go.work", `go 1.18 + use ( + ./package1 + ./package2 + ./package2/vendor/example.com/foo + ) + `) + env.Await(NoOutstandingWork()) + + // Check that definitions in package1 go to the copy vendored in package2. + location, _ := env.GoToDefinition("package1/main.go", env.RegexpSearch("package1/main.go", "CompleteMe")) + const wantLocation = "package2/vendor/example.com/foo/foo.go" + if !strings.HasSuffix(location, wantLocation) { + t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation) + } + }) +} + +// Test for golang/go#43186: correcting the module path should fix errors +// without restarting gopls. +func TestBrokenWorkspace_WrongModulePath(t *testing.T) { + const files = ` +-- go.mod -- +module mod.testx + +go 1.18 +-- p/internal/foo/foo.go -- +package foo + +const C = 1 +-- p/internal/bar/bar.go -- +package bar + +import "mod.test/p/internal/foo" + +const D = foo.C + 1 +-- p/internal/bar/bar_test.go -- +package bar_test + +import ( + "mod.test/p/internal/foo" + . "mod.test/p/internal/bar" +) + +const E = D + foo.C +-- p/internal/baz/baz_test.go -- +package baz_test + +import ( + named "mod.test/p/internal/bar" +) + +const F = named.D - 3 +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("p/internal/bar/bar.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\""), + ), + ) + env.OpenFile("go.mod") + env.RegexpReplace("go.mod", "mod.testx", "mod.test") + env.SaveBuffer("go.mod") // saving triggers a reload + env.Await(NoOutstandingDiagnostics()) + }) +} + +func TestMultipleModules_Warning(t *testing.T) { + msgForVersion := func(ver int) string { + if ver >= 18 { + return `gopls was not able to find modules in your workspace.` + } else { + return `gopls requires a module at the root of your workspace.` + } + } + + const modules = ` +-- a/go.mod -- +module a.com + +go 1.12 +-- a/a.go -- +package a +-- a/empty.go -- +// an empty file +-- b/go.mod -- +module b.com + +go 1.12 +-- b/b.go -- +package b +` + for _, go111module := range []string{"on", "auto"} { + t.Run("GO111MODULE="+go111module, func(t *testing.T) { + WithOptions( + Modes(Default), + EnvVars{"GO111MODULE": go111module}, + ).Run(t, modules, func(t *testing.T, env *Env) { + ver := env.GoVersion() + msg := msgForVersion(ver) + env.OpenFile("a/a.go") + env.OpenFile("a/empty.go") + env.OpenFile("b/go.mod") + env.Await( + env.DiagnosticAtRegexp("a/a.go", "package a"), + env.DiagnosticAtRegexp("b/go.mod", "module b.com"), + OutstandingWork(lsp.WorkspaceLoadFailure, msg), + ) + + // Changing the workspace folders to the valid modules should resolve + // the workspace error. + env.ChangeWorkspaceFolders("a", "b") + env.Await(NoOutstandingWork()) + + env.ChangeWorkspaceFolders(".") + + // TODO(rfindley): when GO111MODULE=auto, we need to open or change a + // file here in order to detect a critical error. This is because gopls + // has forgotten about a/a.go, and therefor doesn't hit the heuristic + // "all packages are command-line-arguments". + // + // This is broken, and could be fixed by adjusting the heuristic to + // account for the scenario where there are *no* workspace packages, or + // (better) trying to get workspace packages for each open file. See + // also golang/go#54261. + env.OpenFile("b/b.go") + env.Await(OutstandingWork(lsp.WorkspaceLoadFailure, msg)) + }) + }) + } + + // Expect no warning if GO111MODULE=auto in a directory in GOPATH. + t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { + WithOptions( + Modes(Default), + EnvVars{"GO111MODULE": "auto"}, + InGOPATH(), + ).Run(t, modules, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + EmptyDiagnostics("a/a.go"), + ), + NoOutstandingWork(), + ) + }) + }) +} diff --git a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go new file mode 100644 index 00000000000..3efa9f322a8 --- /dev/null +++ b/gopls/internal/regtest/workspace/directoryfilters_test.go @@ -0,0 +1,252 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "sort" + "strings" + "testing" + + . "golang.org/x/tools/gopls/internal/lsp/regtest" +) + +// This file contains regression tests for the directoryFilters setting. +// +// TODO: +// - consolidate some of these tests into a single test +// - add more tests for changing directory filters + +func TestDirectoryFilters(t *testing.T) { + WithOptions( + ProxyFiles(workspaceProxy), + WorkspaceFolders("pkg"), + Settings{ + "directoryFilters": []string{"-inner"}, + }, + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { + syms := env.WorkspaceSymbol("Hi") + sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) + for _, s := range syms { + if strings.Contains(s.ContainerName, "inner") { + t.Errorf("WorkspaceSymbol: found symbol %q with container %q, want \"inner\" excluded", s.Name, s.ContainerName) + } + } + }) +} + +func TestDirectoryFiltersLoads(t *testing.T) { + // exclude, and its error, should be excluded from the workspace. + const files = ` +-- go.mod -- +module example.com + +go 1.12 +-- exclude/exclude.go -- +package exclude + +const _ = Nonexistant +` + + WithOptions( + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.Await(NoDiagnostics("exclude/x.go")) + }) +} + +func TestDirectoryFiltersTransitiveDep(t *testing.T) { + // Even though exclude is excluded from the workspace, it should + // still be importable as a non-workspace package. + const files = ` +-- go.mod -- +module example.com + +go 1.12 +-- include/include.go -- +package include +import "example.com/exclude" + +const _ = exclude.X +-- exclude/exclude.go -- +package exclude + +const _ = Nonexistant // should be ignored, since this is a non-workspace package +const X = 1 +` + + WithOptions( + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.Await( + NoDiagnostics("exclude/exclude.go"), // filtered out + NoDiagnostics("include/include.go"), // successfully builds + ) + }) +} + +func TestDirectoryFiltersWorkspaceModules(t *testing.T) { + // Define a module include.com which should be in the workspace, plus a + // module exclude.com which should be excluded and therefore come from + // the proxy. + const files = ` +-- include/go.mod -- +module include.com + +go 1.12 + +require exclude.com v1.0.0 + +-- include/go.sum -- +exclude.com v1.0.0 h1:Q5QSfDXY5qyNCBeUiWovUGqcLCRZKoTs9XdBeVz+w1I= +exclude.com v1.0.0/go.mod h1:hFox2uDlNB2s2Jfd9tHlQVfgqUiLVTmh6ZKat4cvnj4= + +-- include/include.go -- +package include + +import "exclude.com" + +var _ = exclude.X // satisfied only by the workspace version +-- exclude/go.mod -- +module exclude.com + +go 1.12 +-- exclude/exclude.go -- +package exclude + +const X = 1 +` + const proxy = ` +-- exclude.com@v1.0.0/go.mod -- +module exclude.com + +go 1.12 +-- exclude.com@v1.0.0/exclude.go -- +package exclude +` + WithOptions( + Modes(Experimental), + ProxyFiles(proxy), + Settings{"directoryFilters": []string{"-exclude"}}, + ).Run(t, files, func(t *testing.T, env *Env) { + env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`)) + }) +} + +// Test for golang/go#46438: support for '**' in directory filters. +func TestDirectoryFilters_Wildcard(t *testing.T) { + filters := []string{"-**/bye"} + WithOptions( + ProxyFiles(workspaceProxy), + WorkspaceFolders("pkg"), + Settings{ + "directoryFilters": filters, + }, + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { + syms := env.WorkspaceSymbol("Bye") + sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) + for _, s := range syms { + if strings.Contains(s.ContainerName, "bye") { + t.Errorf("WorkspaceSymbol: found symbol %q with container %q with filters %v", s.Name, s.ContainerName, filters) + } + } + }) +} + +// Test for golang/go#52993: wildcard directoryFilters should apply to +// goimports scanning as well. +func TestDirectoryFilters_ImportScanning(t *testing.T) { + const files = ` +-- go.mod -- +module mod.test + +go 1.12 +-- main.go -- +package main + +func main() { + bye.Goodbye() +} +-- p/bye/bye.go -- +package bye + +func Goodbye() {} +` + + WithOptions( + Settings{ + "directoryFilters": []string{"-**/bye"}, + }, + // This test breaks in 'Experimental' mode, because with + // experimentalWorkspaceModule set we the goimports scan behaves + // differently. + // + // Since this feature is going away (golang/go#52897), don't investigate. + Modes(Default), + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + beforeSave := env.Editor.BufferText("main.go") + env.OrganizeImports("main.go") + got := env.Editor.BufferText("main.go") + if got != beforeSave { + t.Errorf("after organizeImports code action, got modified buffer:\n%s", got) + } + }) +} + +// Test for golang/go#52993: non-wildcard directoryFilters should still be +// applied relative to the workspace folder, not the module root. +func TestDirectoryFilters_MultiRootImportScanning(t *testing.T) { + const files = ` +-- go.work -- +go 1.18 + +use ( + a + b +) +-- a/go.mod -- +module mod1.test + +go 1.18 +-- a/main.go -- +package main + +func main() { + hi.Hi() +} +-- a/hi/hi.go -- +package hi + +func Hi() {} +-- b/go.mod -- +module mod2.test + +go 1.18 +-- b/main.go -- +package main + +func main() { + hi.Hi() +} +-- b/hi/hi.go -- +package hi + +func Hi() {} +` + + WithOptions( + Settings{ + "directoryFilters": []string{"-hi"}, // this test fails with -**/hi + }, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("a/main.go") + beforeSave := env.Editor.BufferText("a/main.go") + env.OrganizeImports("a/main.go") + got := env.Editor.BufferText("a/main.go") + if got == beforeSave { + t.Errorf("after organizeImports code action, got identical buffer:\n%s", got) + } + }) +} diff --git a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go new file mode 100644 index 00000000000..83059bc75ce --- /dev/null +++ b/gopls/internal/regtest/workspace/fromenv_test.go @@ -0,0 +1,56 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "testing" + + . "golang.org/x/tools/gopls/internal/lsp/regtest" +) + +// Test that setting go.work via environment variables or settings works. +func TestUseGoWorkOutsideTheWorkspace(t *testing.T) { + const files = ` +-- work/a/go.mod -- +module a.com + +go 1.12 +-- work/a/a.go -- +package a +-- work/b/go.mod -- +module b.com + +go 1.12 +-- work/b/b.go -- +package b + +func _() { + x := 1 // unused +} +-- config/go.work -- +go 1.18 + +use ( + $SANDBOX_WORKDIR/work/a + $SANDBOX_WORKDIR/work/b +) +` + + WithOptions( + EnvVars{"GOWORK": "$SANDBOX_WORKDIR/config/go.work"}, + ).Run(t, files, func(t *testing.T, env *Env) { + // When we have an explicit GOWORK set, we should get a file watch request. + env.Await(FileWatchMatching(`config.go\.work`)) + // Even though work/b is not open, we should get its diagnostics as it is + // included in the workspace. + env.OpenFile("work/a/a.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexpWithMessage("work/b/b.go", "x := 1", "not used"), + ), + ) + }) +} diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go index 28291a2e23d..c5598c93f76 100644 --- a/gopls/internal/regtest/workspace/metadata_test.go +++ b/gopls/internal/regtest/workspace/metadata_test.go @@ -7,7 +7,7 @@ package workspace import ( "testing" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/testenv" ) @@ -41,3 +41,74 @@ const C = 42 )) }) } + +// Test that moving ignoring a file via build constraints causes diagnostics to +// be resolved. +func TestIgnoreFile(t *testing.T) { + testenv.NeedsGo1Point(t, 17) // needs native overlays and support for go:build directives + + const src = ` +-- go.mod -- +module mod.test + +go 1.12 +-- foo.go -- +package main + +func main() {} +-- bar.go -- +package main + +func main() {} + ` + + WithOptions( + // TODO(golang/go#54180): we don't run in 'experimental' mode here, because + // with "experimentalUseInvalidMetadata", this test fails because the + // orphaned bar.go is diagnosed using stale metadata, and then not + // re-diagnosed when new metadata arrives. + // + // We could fix this by re-running diagnostics after a load, but should + // consider whether that is worthwhile. + Modes(Default), + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("foo.go") + env.OpenFile("bar.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("foo.go", "func (main)"), + env.DiagnosticAtRegexp("bar.go", "func (main)"), + ), + ) + + // Ignore bar.go. This should resolve diagnostics. + env.RegexpReplace("bar.go", "package main", "//go:build ignore\n\npackage main") + + // To make this test pass with experimentalUseInvalidMetadata, we could make + // an arbitrary edit that invalidates the snapshot, at which point the + // orphaned diagnostics will be invalidated. + // + // But of course, this should not be necessary: we should invalidate stale + // information when fresh metadata arrives. + // env.RegexpReplace("foo.go", "package main", "package main // test") + env.Await( + OnceMet( + env.DoneWithChange(), + EmptyDiagnostics("foo.go"), + EmptyDiagnostics("bar.go"), + ), + ) + + // If instead of 'ignore' (which gopls treats as a standalone package) we + // used a different build tag, we should get a warning about having no + // packages for bar.go + env.RegexpReplace("bar.go", "ignore", "excluded") + env.Await( + OnceMet( + env.DoneWithChange(), + env.DiagnosticAtRegexpWithMessage("bar.go", "package (main)", "No packages"), + ), + ) + }) +} diff --git a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go new file mode 100644 index 00000000000..86e3441e52d --- /dev/null +++ b/gopls/internal/regtest/workspace/standalone_test.go @@ -0,0 +1,249 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package workspace + +import ( + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/tools/gopls/internal/lsp/protocol" + . "golang.org/x/tools/gopls/internal/lsp/regtest" + "golang.org/x/tools/internal/testenv" +) + +func TestStandaloneFiles(t *testing.T) { + testenv.NeedsGo1Point(t, 16) // Standalone files are only supported at Go 1.16 and later. + + const files = ` +-- go.mod -- +module mod.test + +go 1.16 +-- lib/lib.go -- +package lib + +const C = 0 + +type I interface { + M() +} +-- lib/ignore.go -- +//go:build ignore +// +build ignore + +package main + +import ( + "mod.test/lib" +) + +const C = 1 + +type Mer struct{} +func (Mer) M() + +func main() { + println(lib.C + C) +} +` + WithOptions( + // On Go 1.17 and earlier, this test fails with + // experimentalWorkspaceModule. Not investigated, as + // experimentalWorkspaceModule will be removed. + Modes(Default), + ).Run(t, files, func(t *testing.T, env *Env) { + // Initially, gopls should not know about the standalone file as it hasn't + // been opened. Therefore, we should only find one symbol 'C'. + syms := env.WorkspaceSymbol("C") + if got, want := len(syms), 1; got != want { + t.Errorf("got %d symbols, want %d", got, want) + } + + // Similarly, we should only find one reference to "C", and no + // implementations of I. + checkLocations := func(method string, gotLocations []protocol.Location, wantFiles ...string) { + var gotFiles []string + for _, l := range gotLocations { + gotFiles = append(gotFiles, env.Sandbox.Workdir.URIToPath(l.URI)) + } + sort.Strings(gotFiles) + sort.Strings(wantFiles) + if diff := cmp.Diff(wantFiles, gotFiles); diff != "" { + t.Errorf("%s(...): unexpected locations (-want +got):\n%s", method, diff) + } + } + + env.OpenFile("lib/lib.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + NoOutstandingDiagnostics(), + ), + ) + + // Replacing C with D should not cause any workspace diagnostics, since we + // haven't yet opened the standalone file. + env.RegexpReplace("lib/lib.go", "C", "D") + env.Await( + OnceMet( + env.DoneWithChange(), + NoOutstandingDiagnostics(), + ), + ) + env.RegexpReplace("lib/lib.go", "D", "C") + env.Await( + OnceMet( + env.DoneWithChange(), + NoOutstandingDiagnostics(), + ), + ) + + refs := env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C")) + checkLocations("References", refs, "lib/lib.go") + + impls := env.Implementations("lib/lib.go", env.RegexpSearch("lib/lib.go", "I")) + checkLocations("Implementations", impls) // no implementations + + // Opening the standalone file should not result in any diagnostics. + env.OpenFile("lib/ignore.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + NoOutstandingDiagnostics(), + ), + ) + + // Having opened the standalone file, we should find its symbols in the + // workspace. + syms = env.WorkspaceSymbol("C") + if got, want := len(syms), 2; got != want { + t.Fatalf("got %d symbols, want %d", got, want) + } + + foundMainC := false + var symNames []string + for _, sym := range syms { + symNames = append(symNames, sym.Name) + if sym.Name == "main.C" { + foundMainC = true + } + } + if !foundMainC { + t.Errorf("WorkspaceSymbol(\"C\") = %v, want containing main.C", symNames) + } + + // We should resolve workspace definitions in the standalone file. + file, _ := env.GoToDefinition("lib/ignore.go", env.RegexpSearch("lib/ignore.go", "lib.(C)")) + if got, want := file, "lib/lib.go"; got != want { + t.Errorf("GoToDefinition(lib.C) = %v, want %v", got, want) + } + + // ...as well as intra-file definitions + file, pos := env.GoToDefinition("lib/ignore.go", env.RegexpSearch("lib/ignore.go", "\\+ (C)")) + if got, want := file, "lib/ignore.go"; got != want { + t.Errorf("GoToDefinition(C) = %v, want %v", got, want) + } + wantPos := env.RegexpSearch("lib/ignore.go", "const (C)") + if pos != wantPos { + t.Errorf("GoToDefinition(C) = %v, want %v", pos, wantPos) + } + + // Renaming "lib.C" to "lib.D" should cause a diagnostic in the standalone + // file. + env.RegexpReplace("lib/lib.go", "C", "D") + env.Await( + OnceMet( + env.DoneWithChange(), + env.DiagnosticAtRegexp("lib/ignore.go", "lib.(C)"), + ), + ) + + // Undoing the replacement should fix diagnostics + env.RegexpReplace("lib/lib.go", "D", "C") + env.Await( + OnceMet( + env.DoneWithChange(), + NoOutstandingDiagnostics(), + ), + ) + + // Now that our workspace has no errors, we should be able to find + // references and rename. + refs = env.References("lib/lib.go", env.RegexpSearch("lib/lib.go", "C")) + checkLocations("References", refs, "lib/lib.go", "lib/ignore.go") + + impls = env.Implementations("lib/lib.go", env.RegexpSearch("lib/lib.go", "I")) + checkLocations("Implementations", impls, "lib/ignore.go") + + // Renaming should rename in the standalone package. + env.Rename("lib/lib.go", env.RegexpSearch("lib/lib.go", "C"), "D") + env.RegexpSearch("lib/ignore.go", "lib.D") + }) +} + +func TestStandaloneFiles_Configuration(t *testing.T) { + testenv.NeedsGo1Point(t, 16) // Standalone files are only supported at Go 1.16 and later. + + const files = ` +-- go.mod -- +module mod.test + +go 1.18 +-- lib.go -- +package lib // without this package, files are loaded as command-line-arguments +-- ignore.go -- +//go:build ignore +// +build ignore + +package main + +// An arbitrary comment. + +func main() {} +-- standalone.go -- +//go:build standalone +// +build standalone + +package main + +func main() {} +` + + WithOptions( + Settings{ + "standaloneTags": []string{"standalone", "script"}, + }, + ).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("ignore.go") + env.OpenFile("standalone.go") + + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexp("ignore.go", "package (main)"), + EmptyOrNoDiagnostics("standalone.go"), + ), + ) + + cfg := env.Editor.Config() + cfg.Settings = map[string]interface{}{ + "standaloneTags": []string{"ignore"}, + } + env.ChangeConfiguration(cfg) + + // TODO(golang/go#56158): gopls does not purge previously published + // diagnostice when configuration changes. + env.RegexpReplace("ignore.go", "arbitrary", "meaningless") + + env.Await( + OnceMet( + env.DoneWithChange(), + EmptyOrNoDiagnostics("ignore.go"), + env.DiagnosticAtRegexp("standalone.go", "package (main)"), + ), + ) + }) +} diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go index 5e5bcd13b5d..5786f0a031b 100644 --- a/gopls/internal/regtest/workspace/workspace_test.go +++ b/gopls/internal/regtest/workspace/workspace_test.go @@ -5,20 +5,21 @@ package workspace import ( + "context" "fmt" "path/filepath" - "sort" "strings" "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/testenv" - . "golang.org/x/tools/internal/lsp/regtest" + . "golang.org/x/tools/gopls/internal/lsp/regtest" ) func TestMain(m *testing.M) { @@ -34,6 +35,8 @@ go 1.12 -- example.com@v1.2.3/blah/blah.go -- package blah +import "fmt" + func SaySomething() { fmt.Println("something") } @@ -61,7 +64,7 @@ require ( random.org v1.2.3 ) -- pkg/go.sum -- -example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= +example.com v1.2.3 h1:veRD4tUnatQRgsULqULZPjeoBGFr2qBhevSCZllD2Ds= example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= random.org v1.2.3 h1:+JE2Fkp7gS0zsHXGEQJ7hraom3pNTlkxC4b2qPfA+/Q= random.org v1.2.3/go.mod h1:E9KM6+bBX2g5ykHZ9H27w16sWo3QwgonyjM44Dnej3I= @@ -138,38 +141,6 @@ func TestReferences(t *testing.T) { } } -// make sure that directory filters work -func TestFilters(t *testing.T) { - for _, tt := range []struct { - name, rootPath string - }{ - { - name: "module root", - rootPath: "pkg", - }, - } { - t.Run(tt.name, func(t *testing.T) { - opts := []RunOption{ProxyFiles(workspaceProxy)} - if tt.rootPath != "" { - opts = append(opts, WorkspaceFolders(tt.rootPath)) - } - f := func(o *source.Options) { - o.DirectoryFilters = append(o.DirectoryFilters, "-inner") - } - opts = append(opts, Options(f)) - WithOptions(opts...).Run(t, workspaceModule, func(t *testing.T, env *Env) { - syms := env.WorkspaceSymbol("Hi") - sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) - for i, s := range syms { - if strings.Contains(s.ContainerName, "/inner") { - t.Errorf("%s %v %s %s %d\n", s.Name, s.Kind, s.ContainerName, tt.name, i) - } - } - }) - }) - } -} - // Make sure that analysis diagnostics are cleared for the whole package when // the only opened file is closed. This test was inspired by the experience in // VS Code, where clicking on a reference result triggers a @@ -190,6 +161,31 @@ func TestClearAnalysisDiagnostics(t *testing.T) { }) } +// TestReloadOnlyOnce checks that changes to the go.mod file do not result in +// redundant package loads (golang/go#54473). +// +// Note that this test may be fragile, as it depends on specific structure to +// log messages around reinitialization. Nevertheless, it is important for +// guarding against accidentally duplicate reloading. +func TestReloadOnlyOnce(t *testing.T) { + WithOptions( + ProxyFiles(workspaceProxy), + WorkspaceFolders("pkg"), + ).Run(t, workspaceModule, func(t *testing.T, env *Env) { + dir := env.Sandbox.Workdir.URI("goodbye").SpanURI().Filename() + goModWithReplace := fmt.Sprintf(`%s +replace random.org => %s +`, env.ReadWorkspaceFile("pkg/go.mod"), dir) + env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace) + env.Await( + OnceMet( + env.DoneWithChangeWatchedFiles(), + LogMatching(protocol.Info, `packages\.Load #\d+\n`, 2, false), + ), + ) + }) +} + // This test checks that gopls updates the set of files it watches when a // replace target is added to the go.mod. func TestWatchReplaceTargets(t *testing.T) { @@ -222,6 +218,8 @@ go 1.12 -- example.com@v1.2.3/blah/blah.go -- package blah +import "fmt" + func SaySomething() { fmt.Println("something") } @@ -290,6 +288,8 @@ require b.com v1.2.3 -- c.com@v1.2.3/blah/blah.go -- package blah +import "fmt" + func SaySomething() { fmt.Println("something") } @@ -305,10 +305,6 @@ func Hello() {} module b.com go 1.12 --- b.com@v1.2.4/b/b.go -- -package b - -func Hello() {} ` const multiModule = ` -- go.mod -- @@ -339,8 +335,11 @@ func main() { // This change tests that the version of the module used changes after it has // been deleted from the workspace. +// +// TODO(golang/go#55331): delete this placeholder along with experimental +// workspace module. func TestDeleteModule_Interdependent(t *testing.T) { - t.Skip("Skipping due to golang/go#46375: race due to orphaned file reloading") + t.Skip("golang/go#55331: the experimental workspace module is scheduled for deletion") const multiModule = ` -- moda/a/go.mod -- @@ -391,15 +390,6 @@ func Hello() int { env.DoneWithChangeWatchedFiles(), ) - d := protocol.PublishDiagnosticsParams{} - env.Await( - OnceMet( - env.DiagnosticAtRegexpWithMessage("moda/a/go.mod", "require b.com v1.2.3", "b.com@v1.2.3 has not been downloaded"), - ReadDiagnostics("moda/a/go.mod", &d), - ), - ) - env.ApplyQuickFixes("moda/a/go.mod", d.Diagnostics) - env.Await(env.DoneWithChangeWatchedFiles()) got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello")) if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { t.Errorf("expected %s, got %v", want, got) @@ -539,7 +529,7 @@ module b.com require example.com v1.2.3 -- modb/go.sum -- -example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= +example.com v1.2.3 h1:veRD4tUnatQRgsULqULZPjeoBGFr2qBhevSCZllD2Ds= example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= -- modb/b/b.go -- package b @@ -595,21 +585,16 @@ require ( replace a.com => %s/moda/a replace b.com => %s/modb `, workdir, workdir)) - env.Await(env.DoneWithChangeWatchedFiles()) - // Check that go.mod diagnostics picked up the newly active mod file. - // The local version of modb has an extra dependency we need to download. - env.OpenFile("modb/go.mod") - env.Await(env.DoneWithOpen()) - var d protocol.PublishDiagnosticsParams + // As of golang/go#54069, writing a gopls.mod to the workspace triggers a + // workspace reload. env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("modb/go.mod", `require example.com v1.2.3`, "has not been downloaded"), - ReadDiagnostics("modb/go.mod", &d), + env.DoneWithChangeWatchedFiles(), + env.DiagnosticAtRegexp("modb/b/b.go", "x"), ), ) - env.ApplyQuickFixes("modb/go.mod", d.Diagnostics) - env.Await(env.DiagnosticAtRegexp("modb/b/b.go", "x")) + // Jumping to definition should now go to b.com in the workspace. if err := checkHelloLocation("modb/b/b.go"); err != nil { t.Fatal(err) @@ -736,21 +721,15 @@ use ( ./modb ) `) - env.Await(env.DoneWithChangeWatchedFiles()) - // Check that go.mod diagnostics picked up the newly active mod file. - // The local version of modb has an extra dependency we need to download. - env.OpenFile("modb/go.mod") - env.Await(env.DoneWithOpen()) - var d protocol.PublishDiagnosticsParams + // As of golang/go#54069, writing go.work to the workspace triggers a + // workspace reload. env.Await( OnceMet( - env.DiagnosticAtRegexpWithMessage("modb/go.mod", `require example.com v1.2.3`, "has not been downloaded"), - ReadDiagnostics("modb/go.mod", &d), + env.DoneWithChangeWatchedFiles(), + env.DiagnosticAtRegexp("modb/b/b.go", "x"), ), ) - env.ApplyQuickFixes("modb/go.mod", d.Diagnostics) - env.Await(env.DiagnosticAtRegexp("modb/b/b.go", "x")) // Jumping to definition should now go to b.com in the workspace. if err := checkHelloLocation("modb/b/b.go"); err != nil { @@ -1024,104 +1003,6 @@ func main() { }) } -func TestDirectoryFiltersLoads(t *testing.T) { - // exclude, and its error, should be excluded from the workspace. - const files = ` --- go.mod -- -module example.com - -go 1.12 --- exclude/exclude.go -- -package exclude - -const _ = Nonexistant -` - cfg := EditorConfig{ - DirectoryFilters: []string{"-exclude"}, - } - WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) { - env.Await(NoDiagnostics("exclude/x.go")) - }) -} - -func TestDirectoryFiltersTransitiveDep(t *testing.T) { - // Even though exclude is excluded from the workspace, it should - // still be importable as a non-workspace package. - const files = ` --- go.mod -- -module example.com - -go 1.12 --- include/include.go -- -package include -import "example.com/exclude" - -const _ = exclude.X --- exclude/exclude.go -- -package exclude - -const _ = Nonexistant // should be ignored, since this is a non-workspace package -const X = 1 -` - - cfg := EditorConfig{ - DirectoryFilters: []string{"-exclude"}, - } - WithOptions(cfg).Run(t, files, func(t *testing.T, env *Env) { - env.Await( - NoDiagnostics("exclude/exclude.go"), // filtered out - NoDiagnostics("include/include.go"), // successfully builds - ) - }) -} - -func TestDirectoryFiltersWorkspaceModules(t *testing.T) { - // Define a module include.com which should be in the workspace, plus a - // module exclude.com which should be excluded and therefore come from - // the proxy. - const files = ` --- include/go.mod -- -module include.com - -go 1.12 - -require exclude.com v1.0.0 - --- include/go.sum -- -exclude.com v1.0.0 h1:Q5QSfDXY5qyNCBeUiWovUGqcLCRZKoTs9XdBeVz+w1I= -exclude.com v1.0.0/go.mod h1:hFox2uDlNB2s2Jfd9tHlQVfgqUiLVTmh6ZKat4cvnj4= - --- include/include.go -- -package include - -import "exclude.com" - -var _ = exclude.X // satisfied only by the workspace version --- exclude/go.mod -- -module exclude.com - -go 1.12 --- exclude/exclude.go -- -package exclude - -const X = 1 -` - const proxy = ` --- exclude.com@v1.0.0/go.mod -- -module exclude.com - -go 1.12 --- exclude.com@v1.0.0/exclude.go -- -package exclude -` - cfg := EditorConfig{ - DirectoryFilters: []string{"-exclude"}, - } - WithOptions(cfg, Modes(Experimental), ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) { - env.Await(env.DiagnosticAtRegexp("include/include.go", `exclude.(X)`)) - }) -} - // Confirm that a fix for a tidy module will correct all modules in the // workspace. func TestMultiModule_OneBrokenModule(t *testing.T) { @@ -1204,10 +1085,8 @@ go 1.12 package main ` WithOptions( - EditorConfig{Env: map[string]string{ - "GOPATH": filepath.FromSlash("$SANDBOX_WORKDIR/gopath"), - }}, - Modes(Singleton), + EnvVars{"GOPATH": filepath.FromSlash("$SANDBOX_WORKDIR/gopath")}, + Modes(Default), ).Run(t, mod, func(t *testing.T, env *Env) { env.Await( // Confirm that the build configuration is seen as valid, @@ -1238,7 +1117,7 @@ package main func main() {} ` WithOptions( - Modes(Singleton), + Modes(Default), ).Run(t, nomod, func(t *testing.T, env *Env) { env.OpenFile("a/main.go") env.OpenFile("b/main.go") @@ -1292,8 +1171,8 @@ func (Server) Foo() {} // as invalid. So we need to wait for the metadata of main_test.go to be // updated before moving other_test.go back to the main_test package. env.Await( - env.DiagnosticAtRegexpWithMessage("other_test.go", "Server", "undeclared"), - env.DiagnosticAtRegexpWithMessage("main_test.go", "otherConst", "undeclared"), + env.DiagnosticAtRegexp("other_test.go", "Server"), + env.DiagnosticAtRegexp("main_test.go", "otherConst"), ) env.RegexpReplace("other_test.go", "main", "main_test") env.Await( @@ -1305,3 +1184,141 @@ func (Server) Foo() {} _, _ = env.GoToDefinition("other_test.go", env.RegexpSearch("other_test.go", "Server")) }) } + +// Test for golang/go#48929. +func TestClearNonWorkspaceDiagnostics(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // uses go.work + + const ws = ` +-- go.work -- +go 1.18 + +use ( + ./b +) +-- a/go.mod -- +module a + +go 1.17 +-- a/main.go -- +package main + +func main() { + var V string +} +-- b/go.mod -- +module b + +go 1.17 +-- b/main.go -- +package b + +import ( + _ "fmt" +) +` + Run(t, ws, func(t *testing.T, env *Env) { + env.OpenFile("b/main.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + NoDiagnostics("a/main.go"), + ), + ) + env.OpenFile("a/main.go") + env.Await( + OnceMet( + env.DoneWithOpen(), + env.DiagnosticAtRegexpWithMessage("a/main.go", "V", "not used"), + ), + ) + env.CloseBuffer("a/main.go") + + // Make an arbitrary edit because gopls explicitly diagnoses a/main.go + // whenever it is "changed". + // + // TODO(rfindley): it should not be necessary to make another edit here. + // Gopls should be smart enough to avoid diagnosing a. + env.RegexpReplace("b/main.go", "package b", "package b // a package") + env.Await( + OnceMet( + env.DoneWithChange(), + EmptyDiagnostics("a/main.go"), + ), + ) + }) +} + +// Test that we don't get a version warning when the Go version in PATH is +// supported. +func TestOldGoNotification_SupportedVersion(t *testing.T) { + v := goVersion(t) + if v < lsp.OldestSupportedGoVersion() { + t.Skipf("go version 1.%d is unsupported", v) + } + + Run(t, "", func(t *testing.T, env *Env) { + env.Await( + OnceMet( + InitialWorkspaceLoad, + NoShownMessage("upgrade"), + ), + ) + }) +} + +// Test that we do get a version warning when the Go version in PATH is +// unsupported, though this test may never execute if we stop running CI at +// legacy Go versions (see also TestOldGoNotification_Fake) +func TestOldGoNotification_UnsupportedVersion(t *testing.T) { + v := goVersion(t) + if v >= lsp.OldestSupportedGoVersion() { + t.Skipf("go version 1.%d is supported", v) + } + + Run(t, "", func(t *testing.T, env *Env) { + env.Await( + // Note: cannot use OnceMet(InitialWorkspaceLoad, ...) here, as the + // upgrade message may race with the IWL. + ShownMessage("Please upgrade"), + ) + }) +} + +func TestOldGoNotification_Fake(t *testing.T) { + // Get the Go version from path, and make sure it's unsupported. + // + // In the future we'll stop running CI on legacy Go versions. By mutating the + // oldest supported Go version here, we can at least ensure that the + // ShowMessage pop-up works. + ctx := context.Background() + goversion, err := gocommand.GoVersion(ctx, gocommand.Invocation{}, &gocommand.Runner{}) + if err != nil { + t.Fatal(err) + } + defer func(t []lsp.GoVersionSupport) { + lsp.GoVersionTable = t + }(lsp.GoVersionTable) + lsp.GoVersionTable = []lsp.GoVersionSupport{ + {GoVersion: goversion, InstallGoplsVersion: "v1.0.0"}, + } + + Run(t, "", func(t *testing.T, env *Env) { + env.Await( + // Note: cannot use OnceMet(InitialWorkspaceLoad, ...) here, as the + // upgrade message may race with the IWL. + ShownMessage("Please upgrade"), + ) + }) +} + +// goVersion returns the version of the Go command in PATH. +func goVersion(t *testing.T) int { + t.Helper() + ctx := context.Background() + goversion, err := gocommand.GoVersion(ctx, gocommand.Invocation{}, &gocommand.Runner{}) + if err != nil { + t.Fatal(err) + } + return goversion +} diff --git a/gopls/internal/robustio/copyfiles.go b/gopls/internal/robustio/copyfiles.go new file mode 100644 index 00000000000..6e9f4b3875f --- /dev/null +++ b/gopls/internal/robustio/copyfiles.go @@ -0,0 +1,117 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore +// +build ignore + +// The copyfiles script copies the contents of the internal cmd/go robustio +// package to the current directory, with adjustments to make it build. +// +// NOTE: In retrospect this script got out of hand, as we have to perform +// various operations on the package to get it to build at old Go versions. If +// in the future it proves to be flaky, delete it and just copy code manually. +package main + +import ( + "bytes" + "go/build/constraint" + "go/scanner" + "go/token" + "log" + "os" + "path/filepath" + "runtime" + "strings" +) + +func main() { + dir := filepath.Join(runtime.GOROOT(), "src", "cmd", "go", "internal", "robustio") + + entries, err := os.ReadDir(dir) + if err != nil { + log.Fatalf("reading the robustio dir: %v", err) + } + + // Collect file content so that we can validate before copying. + fileContent := make(map[string][]byte) + windowsImport := []byte("\t\"internal/syscall/windows\"\n") + foundWindowsImport := false + for _, entry := range entries { + if strings.HasSuffix(entry.Name(), ".go") { + pth := filepath.Join(dir, entry.Name()) + content, err := os.ReadFile(pth) + if err != nil { + log.Fatalf("reading %q: %v", entry.Name(), err) + } + + // Replace the use of internal/syscall/windows.ERROR_SHARING_VIOLATION + // with a local constant. + if entry.Name() == "robustio_windows.go" && bytes.Contains(content, windowsImport) { + foundWindowsImport = true + content = bytes.Replace(content, windowsImport, nil, 1) + content = bytes.Replace(content, []byte("windows.ERROR_SHARING_VIOLATION"), []byte("ERROR_SHARING_VIOLATION"), -1) + } + + // Replace os.ReadFile with ioutil.ReadFile (for 1.15 and older). We + // attempt to match calls (via the '('), to avoid matching mentions of + // os.ReadFile in comments. + // + // TODO(rfindley): once we (shortly!) no longer support 1.15, remove + // this and break the build. + if bytes.Contains(content, []byte("os.ReadFile(")) { + content = bytes.Replace(content, []byte("\"os\""), []byte("\"io/ioutil\"\n\t\"os\""), 1) + content = bytes.Replace(content, []byte("os.ReadFile("), []byte("ioutil.ReadFile("), -1) + } + + // Add +build constraints, for 1.16. + content = addPlusBuildConstraints(content) + + fileContent[entry.Name()] = content + } + } + + if !foundWindowsImport { + log.Fatal("missing expected import of internal/syscall/windows in robustio_windows.go") + } + + for name, content := range fileContent { + if err := os.WriteFile(name, content, 0644); err != nil { + log.Fatalf("writing %q: %v", name, err) + } + } +} + +// addPlusBuildConstraints splices in +build constraints for go:build +// constraints encountered in the source. +// +// Gopls still builds at Go 1.16, which requires +build constraints. +func addPlusBuildConstraints(src []byte) []byte { + var s scanner.Scanner + fset := token.NewFileSet() + file := fset.AddFile("", fset.Base(), len(src)) + s.Init(file, src, nil /* no error handler */, scanner.ScanComments) + + result := make([]byte, 0, len(src)) + lastInsertion := 0 + for { + pos, tok, lit := s.Scan() + if tok == token.EOF { + break + } + if tok == token.COMMENT { + if c, err := constraint.Parse(lit); err == nil { + plusBuild, err := constraint.PlusBuildLines(c) + if err != nil { + log.Fatalf("computing +build constraint for %q: %v", lit, err) + } + insertAt := file.Offset(pos) + len(lit) + result = append(result, src[lastInsertion:insertAt]...) + result = append(result, []byte("\n"+strings.Join(plusBuild, "\n"))...) + lastInsertion = insertAt + } + } + } + result = append(result, src[lastInsertion:]...) + return result +} diff --git a/gopls/internal/robustio/gopls.go b/gopls/internal/robustio/gopls.go new file mode 100644 index 00000000000..949f2781619 --- /dev/null +++ b/gopls/internal/robustio/gopls.go @@ -0,0 +1,16 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package robustio + +import "syscall" + +// The robustio package is copied from cmd/go/internal/robustio, a package used +// by the go command to retry known flaky operations on certain operating systems. + +//go:generate go run copyfiles.go + +// Since the gopls module cannot access internal/syscall/windows, copy a +// necessary constant. +const ERROR_SHARING_VIOLATION syscall.Errno = 32 diff --git a/gopls/internal/robustio/robustio.go b/gopls/internal/robustio/robustio.go new file mode 100644 index 00000000000..15b33773cf5 --- /dev/null +++ b/gopls/internal/robustio/robustio.go @@ -0,0 +1,53 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package robustio wraps I/O functions that are prone to failure on Windows, +// transparently retrying errors up to an arbitrary timeout. +// +// Errors are classified heuristically and retries are bounded, so the functions +// in this package do not completely eliminate spurious errors. However, they do +// significantly reduce the rate of failure in practice. +// +// If so, the error will likely wrap one of: +// The functions in this package do not completely eliminate spurious errors, +// but substantially reduce their rate of occurrence in practice. +package robustio + +// Rename is like os.Rename, but on Windows retries errors that may occur if the +// file is concurrently read or overwritten. +// +// (See golang.org/issue/31247 and golang.org/issue/32188.) +func Rename(oldpath, newpath string) error { + return rename(oldpath, newpath) +} + +// ReadFile is like os.ReadFile, but on Windows retries errors that may +// occur if the file is concurrently replaced. +// +// (See golang.org/issue/31247 and golang.org/issue/32188.) +func ReadFile(filename string) ([]byte, error) { + return readFile(filename) +} + +// RemoveAll is like os.RemoveAll, but on Windows retries errors that may occur +// if an executable file in the directory has recently been executed. +// +// (See golang.org/issue/19491.) +func RemoveAll(path string) error { + return removeAll(path) +} + +// IsEphemeralError reports whether err is one of the errors that the functions +// in this package attempt to mitigate. +// +// Errors considered ephemeral include: +// - syscall.ERROR_ACCESS_DENIED +// - syscall.ERROR_FILE_NOT_FOUND +// - internal/syscall/windows.ERROR_SHARING_VIOLATION +// +// This set may be expanded in the future; programs must not rely on the +// non-ephemerality of any given error. +func IsEphemeralError(err error) bool { + return isEphemeralError(err) +} diff --git a/gopls/internal/robustio/robustio_darwin.go b/gopls/internal/robustio/robustio_darwin.go new file mode 100644 index 00000000000..99fd8ebc2ff --- /dev/null +++ b/gopls/internal/robustio/robustio_darwin.go @@ -0,0 +1,21 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package robustio + +import ( + "errors" + "syscall" +) + +const errFileNotFound = syscall.ENOENT + +// isEphemeralError returns true if err may be resolved by waiting. +func isEphemeralError(err error) bool { + var errno syscall.Errno + if errors.As(err, &errno) { + return errno == errFileNotFound + } + return false +} diff --git a/gopls/internal/robustio/robustio_flaky.go b/gopls/internal/robustio/robustio_flaky.go new file mode 100644 index 00000000000..c6f99724468 --- /dev/null +++ b/gopls/internal/robustio/robustio_flaky.go @@ -0,0 +1,93 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows || darwin +// +build windows darwin + +package robustio + +import ( + "errors" + "io/ioutil" + "math/rand" + "os" + "syscall" + "time" +) + +const arbitraryTimeout = 2000 * time.Millisecond + +// retry retries ephemeral errors from f up to an arbitrary timeout +// to work around filesystem flakiness on Windows and Darwin. +func retry(f func() (err error, mayRetry bool)) error { + var ( + bestErr error + lowestErrno syscall.Errno + start time.Time + nextSleep time.Duration = 1 * time.Millisecond + ) + for { + err, mayRetry := f() + if err == nil || !mayRetry { + return err + } + + var errno syscall.Errno + if errors.As(err, &errno) && (lowestErrno == 0 || errno < lowestErrno) { + bestErr = err + lowestErrno = errno + } else if bestErr == nil { + bestErr = err + } + + if start.IsZero() { + start = time.Now() + } else if d := time.Since(start) + nextSleep; d >= arbitraryTimeout { + break + } + time.Sleep(nextSleep) + nextSleep += time.Duration(rand.Int63n(int64(nextSleep))) + } + + return bestErr +} + +// rename is like os.Rename, but retries ephemeral errors. +// +// On Windows it wraps os.Rename, which (as of 2019-06-04) uses MoveFileEx with +// MOVEFILE_REPLACE_EXISTING. +// +// Windows also provides a different system call, ReplaceFile, +// that provides similar semantics, but perhaps preserves more metadata. (The +// documentation on the differences between the two is very sparse.) +// +// Empirical error rates with MoveFileEx are lower under modest concurrency, so +// for now we're sticking with what the os package already provides. +func rename(oldpath, newpath string) (err error) { + return retry(func() (err error, mayRetry bool) { + err = os.Rename(oldpath, newpath) + return err, isEphemeralError(err) + }) +} + +// readFile is like os.ReadFile, but retries ephemeral errors. +func readFile(filename string) ([]byte, error) { + var b []byte + err := retry(func() (err error, mayRetry bool) { + b, err = ioutil.ReadFile(filename) + + // Unlike in rename, we do not retry errFileNotFound here: it can occur + // as a spurious error, but the file may also genuinely not exist, so the + // increase in robustness is probably not worth the extra latency. + return err, isEphemeralError(err) && !errors.Is(err, errFileNotFound) + }) + return b, err +} + +func removeAll(path string) error { + return retry(func() (err error, mayRetry bool) { + err = os.RemoveAll(path) + return err, isEphemeralError(err) + }) +} diff --git a/gopls/internal/robustio/robustio_other.go b/gopls/internal/robustio/robustio_other.go new file mode 100644 index 00000000000..c11dbf9f14b --- /dev/null +++ b/gopls/internal/robustio/robustio_other.go @@ -0,0 +1,29 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !windows && !darwin +// +build !windows,!darwin + +package robustio + +import ( + "io/ioutil" + "os" +) + +func rename(oldpath, newpath string) error { + return os.Rename(oldpath, newpath) +} + +func readFile(filename string) ([]byte, error) { + return ioutil.ReadFile(filename) +} + +func removeAll(path string) error { + return os.RemoveAll(path) +} + +func isEphemeralError(err error) bool { + return false +} diff --git a/gopls/internal/robustio/robustio_windows.go b/gopls/internal/robustio/robustio_windows.go new file mode 100644 index 00000000000..d16e976aad6 --- /dev/null +++ b/gopls/internal/robustio/robustio_windows.go @@ -0,0 +1,26 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package robustio + +import ( + "errors" + "syscall" +) + +const errFileNotFound = syscall.ERROR_FILE_NOT_FOUND + +// isEphemeralError returns true if err may be resolved by waiting. +func isEphemeralError(err error) bool { + var errno syscall.Errno + if errors.As(err, &errno) { + switch errno { + case syscall.ERROR_ACCESS_DENIED, + syscall.ERROR_FILE_NOT_FOUND, + ERROR_SHARING_VIOLATION: + return true + } + } + return false +} diff --git a/internal/span/parse.go b/gopls/internal/span/parse.go similarity index 100% rename from internal/span/parse.go rename to gopls/internal/span/parse.go diff --git a/internal/span/span.go b/gopls/internal/span/span.go similarity index 92% rename from internal/span/span.go rename to gopls/internal/span/span.go index 502145bbea7..333048ae8c6 100644 --- a/internal/span/span.go +++ b/gopls/internal/span/span.go @@ -42,7 +42,7 @@ var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}} var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}} -func New(uri URI, start Point, end Point) Span { +func New(uri URI, start, end Point) Span { s := Span{v: span{URI: uri, Start: start.v, End: end.v}} s.v.clean() return s @@ -209,7 +209,8 @@ func (s Span) Format(f fmt.State, c rune) { } } -func (s Span) WithPosition(tf *token.File) (Span, error) { +// (Currently unused, but we gain little yet by deleting it.) +func (s Span) withPosition(tf *token.File) (Span, error) { if err := s.update(tf, true, false); err != nil { return Span{}, err } @@ -275,3 +276,13 @@ func (p *point) updateOffset(tf *token.File) error { p.Offset = offset return nil } + +// SetRange implements packagestest.rangeSetter, allowing +// gopls' test suites to use Spans instead of Range in parameters. +func (span *Span) SetRange(file *token.File, start, end token.Pos) { + point := func(pos token.Pos) Point { + posn := file.Position(pos) + return NewPoint(posn.Line, posn.Column, posn.Offset) + } + *span = New(URIFromPath(file.Name()), point(start), point(end)) +} diff --git a/internal/span/span_test.go b/gopls/internal/span/span_test.go similarity index 97% rename from internal/span/span_test.go rename to gopls/internal/span/span_test.go index cff59c3d116..63c0752f959 100644 --- a/internal/span/span_test.go +++ b/gopls/internal/span/span_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) var ( diff --git a/internal/span/token.go b/gopls/internal/span/token.go similarity index 64% rename from internal/span/token.go rename to gopls/internal/span/token.go index af01d7b8348..6bde300240f 100644 --- a/internal/span/token.go +++ b/gopls/internal/span/token.go @@ -8,32 +8,49 @@ import ( "fmt" "go/token" - "golang.org/x/tools/internal/lsp/bug" + "golang.org/x/tools/internal/bug" ) // Range represents a source code range in token.Pos form. -// It also carries the FileSet that produced the positions, so that it is +// It also carries the token.File that produced the positions, so that it is // self contained. type Range struct { - Start token.Pos - End token.Pos - - // TokFile may be nil if Start or End is invalid. - // TODO: Eventually we should guarantee that it is non-nil. - TokFile *token.File + TokFile *token.File // non-nil + Start, End token.Pos // both IsValid() } -// NewRange creates a new Range from a FileSet and two positions. -// To represent a point pass a 0 as the end pos. -func NewRange(fset *token.FileSet, start, end token.Pos) Range { - tf := fset.File(start) - if tf == nil { - bug.Reportf("nil file") +// NewRange creates a new Range from a token.File and two positions within it. +// The given start position must be valid; if end is invalid, start is used as +// the end position. +// +// (If you only have a token.FileSet, use file = fset.File(start). But +// most callers know exactly which token.File they're dealing with and +// should pass it explicitly. Not only does this save a lookup, but it +// brings us a step closer to eliminating the global FileSet.) +func NewRange(file *token.File, start, end token.Pos) Range { + if file == nil { + panic("nil *token.File") + } + if !start.IsValid() { + panic("invalid start token.Pos") } + if !end.IsValid() { + end = start + } + + // TODO(adonovan): ideally we would make this stronger assertion: + // + // // Assert that file is non-nil and contains start and end. + // _ = file.Offset(start) + // _ = file.Offset(end) + // + // but some callers (e.g. packageCompletionSurrounding, + // posToMappedRange) don't ensure this precondition. + return Range{ + TokFile: file, Start: start, End: end, - TokFile: tf, } } @@ -64,6 +81,9 @@ func (r Range) Span() (Span, error) { // line directives they may reference positions in another file. If srcFile is // provided, it is used to map the line:column positions referenced by start // and end to offsets in the corresponding file. +// +// TODO(adonovan): clarify whether it is valid to pass posFile==srcFile when +// //line directives are in use. If so, fix this function; if not, fix Range.Span. func FileSpan(posFile, srcFile *token.File, start, end token.Pos) (Span, error) { if !start.IsValid() { return Span{}, fmt.Errorf("start pos is not valid") @@ -99,7 +119,16 @@ func FileSpan(posFile, srcFile *token.File, start, end token.Pos) (Span, error) tf = srcFile } if startFilename != tf.Name() { - return Span{}, bug.Errorf("must supply Converter for file %q", startFilename) + // 'start' identifies a position specified by a //line directive + // in a file other than the one containing the directive. + // (Debugging support for https://github.com/golang/go/issues/54655.) + // + // This used to be a bug.Errorf, but that was unsound because + // Range.Span passes this function the same TokFile argument twice, + // which is never going to pass this test for a file containing + // a //line directive. + // TODO(adonovan): decide where the bug.Errorf really belongs. + return Span{}, fmt.Errorf("must supply Converter for file %q (tf.Name() = %q)", startFilename, tf.Name()) } return s.WithOffset(tf) } @@ -114,7 +143,7 @@ func position(tf *token.File, pos token.Pos) (string, int, int, error) { func positionFromOffset(tf *token.File, offset int) (string, int, int, error) { if offset > tf.Size() { - return "", 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, tf.Size()) + return "", 0, 0, fmt.Errorf("offset %d is beyond EOF (%d) in file %s", offset, tf.Size(), tf.Name()) } pos := tf.Pos(offset) p := tf.Position(pos) @@ -164,15 +193,16 @@ func ToPosition(tf *token.File, offset int) (int, int, error) { return line, col, err } -// ToOffset converts a 1-base line and utf-8 column index into a byte offset in -// the file corresponding to tf. +// ToOffset converts a 1-based line and utf-8 column index into a byte offset +// in the file corresponding to tf. func ToOffset(tf *token.File, line, col int) (int, error) { - if line < 0 { - return -1, fmt.Errorf("line is not valid") + if line < 1 { // token.File.LineStart panics if line < 1 + return -1, fmt.Errorf("invalid line: %d", line) } + lineMax := tf.LineCount() + 1 if line > lineMax { - return -1, fmt.Errorf("line is beyond end of file %v", lineMax) + return -1, fmt.Errorf("line %d is beyond end of file %v", line, lineMax) } else if line == lineMax { if col > 1 { return -1, fmt.Errorf("column is beyond end of file") @@ -182,10 +212,18 @@ func ToOffset(tf *token.File, line, col int) (int, error) { } pos := tf.LineStart(line) if !pos.IsValid() { - return -1, fmt.Errorf("line is not in file") + // bug.Errorf here because LineStart panics on out-of-bound input, and so + // should never return invalid positions. + return -1, bug.Errorf("line is not in file") } // we assume that column is in bytes here, and that the first byte of a // line is at column 1 pos += token.Pos(col - 1) + + // Debugging support for https://github.com/golang/go/issues/54655. + if pos > token.Pos(tf.Base()+tf.Size()) { + return 0, fmt.Errorf("ToOffset: column %d is beyond end of file", col) + } + return offset(tf, pos) } diff --git a/internal/span/token_test.go b/gopls/internal/span/token_test.go similarity index 97% rename from internal/span/token_test.go rename to gopls/internal/span/token_test.go index 1e0b53e1244..997c8fb53ef 100644 --- a/internal/span/token_test.go +++ b/gopls/internal/span/token_test.go @@ -10,7 +10,7 @@ import ( "path" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) var testdata = []struct { diff --git a/internal/span/uri.go b/gopls/internal/span/uri.go similarity index 80% rename from internal/span/uri.go rename to gopls/internal/span/uri.go index a9777ff8598..adcc54a7797 100644 --- a/internal/span/uri.go +++ b/gopls/internal/span/uri.go @@ -38,6 +38,26 @@ func filename(uri URI) (string, error) { if uri == "" { return "", nil } + + // This conservative check for the common case + // of a simple non-empty absolute POSIX filename + // avoids the allocation of a net.URL. + if strings.HasPrefix(string(uri), "file:///") { + rest := string(uri)[len("file://"):] // leave one slash + for i := 0; i < len(rest); i++ { + b := rest[i] + // Reject these cases: + if b < ' ' || b == 0x7f || // control character + b == '%' || b == '+' || // URI escape + b == ':' || // Windows drive letter + b == '@' || b == '&' || b == '?' { // authority or query + goto slow + } + } + return rest, nil + } +slow: + u, err := url.ParseRequestURI(string(uri)) if err != nil { return "", err @@ -50,6 +70,7 @@ func filename(uri URI) (string, error) { if isWindowsDriveURIPath(u.Path) { u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:] } + return u.Path, nil } @@ -80,6 +101,10 @@ func URIFromURI(s string) URI { return URI(u.String()) } +// CompareURI performs a three-valued comparison of two URIs. +// Lexically unequal URIs may compare equal if they are "file:" URIs +// that share the same base name (ignoring case) and denote the same +// file device/inode, according to stat(2). func CompareURI(a, b URI) int { if equalURI(a, b) { return 0 @@ -119,7 +144,9 @@ func equalURI(a, b URI) bool { } // URIFromPath returns a span URI for the supplied file path. -// It will always have the file scheme. +// +// For empty paths, URIFromPath returns the empty URI "". +// For non-empty paths, URIFromPath returns a uri with the file:// scheme. func URIFromPath(path string) URI { if path == "" { return "" @@ -158,7 +185,7 @@ func isWindowsDrivePath(path string) bool { return unicode.IsLetter(rune(path[0])) && path[1] == ':' } -// isWindowsDriveURI returns true if the file URI is of the format used by +// isWindowsDriveURIPath returns true if the file URI is of the format used by // Windows URIs. The url.Parse package does not specially handle Windows paths // (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). func isWindowsDriveURIPath(uri string) bool { diff --git a/internal/span/uri_test.go b/gopls/internal/span/uri_test.go similarity index 98% rename from internal/span/uri_test.go rename to gopls/internal/span/uri_test.go index bcbad87128e..e9904378504 100644 --- a/internal/span/uri_test.go +++ b/gopls/internal/span/uri_test.go @@ -10,7 +10,7 @@ package span_test import ( "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // TestURI tests the conversion between URIs and filenames. The test cases diff --git a/internal/span/uri_windows_test.go b/gopls/internal/span/uri_windows_test.go similarity index 98% rename from internal/span/uri_windows_test.go rename to gopls/internal/span/uri_windows_test.go index e50b58f1bb2..3891e0d3e77 100644 --- a/internal/span/uri_windows_test.go +++ b/gopls/internal/span/uri_windows_test.go @@ -10,7 +10,7 @@ package span_test import ( "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // TestURI tests the conversion between URIs and filenames. The test cases diff --git a/internal/span/utf16.go b/gopls/internal/span/utf16.go similarity index 95% rename from internal/span/utf16.go rename to gopls/internal/span/utf16.go index f4c93a6ead3..0f8e1bcacdf 100644 --- a/internal/span/utf16.go +++ b/gopls/internal/span/utf16.go @@ -13,6 +13,9 @@ import ( // supplied file contents. // This is used to convert from the native (always in bytes) column // representation and the utf16 counts used by some editors. +// +// TODO(adonovan): this function is unused except by its test. Delete, +// or consolidate with (*protocol.ColumnMapper).utf16Column. func ToUTF16Column(p Point, content []byte) (int, error) { if !p.HasPosition() { return -1, fmt.Errorf("ToUTF16Column: point is missing position") diff --git a/internal/span/utf16_test.go b/gopls/internal/span/utf16_test.go similarity index 99% rename from internal/span/utf16_test.go rename to gopls/internal/span/utf16_test.go index 1eae7975bb4..5f75095dcf4 100644 --- a/internal/span/utf16_test.go +++ b/gopls/internal/span/utf16_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/span" ) // The funny character below is 4 bytes long in UTF-8; two UTF-16 code points diff --git a/gopls/internal/vulncheck/command.go b/gopls/internal/vulncheck/command.go index a89354f67ee..e607eb415e6 100644 --- a/gopls/internal/vulncheck/command.go +++ b/gopls/internal/vulncheck/command.go @@ -9,23 +9,28 @@ package vulncheck import ( "context" + "fmt" "log" "os" + "sort" "strings" "golang.org/x/tools/go/packages" gvc "golang.org/x/tools/gopls/internal/govulncheck" - "golang.org/x/tools/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/command" "golang.org/x/vuln/client" + gvcapi "golang.org/x/vuln/exp/govulncheck" + "golang.org/x/vuln/osv" + "golang.org/x/vuln/vulncheck" ) func init() { Govulncheck = govulncheck } -func govulncheck(ctx context.Context, cfg *packages.Config, args command.VulncheckArgs) (res command.VulncheckResult, _ error) { - if args.Pattern == "" { - args.Pattern = "." +func govulncheck(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) { + if patterns == "" { + patterns = "." } dbClient, err := client.NewClient(findGOVULNDB(cfg), client.Options{HTTPCache: gvc.DefaultCache()}) @@ -34,7 +39,7 @@ func govulncheck(ctx context.Context, cfg *packages.Config, args command.Vulnche } c := cmd{Client: dbClient} - vulns, err := c.Run(ctx, cfg, args.Pattern) + vulns, err := c.Run(ctx, cfg, patterns) if err != nil { return res, err } @@ -67,41 +72,99 @@ type cmd struct { // Run runs the govulncheck after loading packages using the provided packages.Config. func (c *cmd) Run(ctx context.Context, cfg *packages.Config, patterns ...string) (_ []Vuln, err error) { + logger := log.New(log.Default().Writer(), "", 0) cfg.Mode |= packages.NeedModule | packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedDeps - log.Println("loading packages...") + logger.Println("loading packages...") loadedPkgs, err := gvc.LoadPackages(cfg, patterns...) if err != nil { - log.Printf("package load failed: %v", err) - return nil, err + logger.Printf("%v", err) + return nil, fmt.Errorf("package load failed") } - log.Printf("loaded %d packages\n", len(loadedPkgs)) - r, err := gvc.Source(ctx, loadedPkgs, c.Client) + logger.Printf("analyzing %d packages...\n", len(loadedPkgs)) + + r, err := vulncheck.Source(ctx, loadedPkgs, &vulncheck.Config{Client: c.Client, SourceGoVersion: goVersion()}) if err != nil { return nil, err } + + logger.Printf("selecting affecting vulnerabilities from %d findings...\n", len(r.Vulns)) + unaffectedMods := filterUnaffected(r.Vulns) + r.Vulns = filterCalled(r) + + logger.Printf("found %d vulnerabilities.\n", len(r.Vulns)) callInfo := gvc.GetCallInfo(r, loadedPkgs) - return toVulns(callInfo) + return toVulns(callInfo, unaffectedMods) // TODO: add import graphs. } -func toVulns(ci *gvc.CallInfo) ([]Vuln, error) { +// filterCalled returns vulnerabilities where the symbols are actually called. +func filterCalled(r *vulncheck.Result) []*vulncheck.Vuln { + var vulns []*vulncheck.Vuln + for _, v := range r.Vulns { + if v.CallSink != 0 { + vulns = append(vulns, v) + } + } + return vulns +} + +// filterUnaffected returns vulnerabilities where no symbols are called, +// grouped by module. +func filterUnaffected(vulns []*vulncheck.Vuln) map[string][]*osv.Entry { + // It is possible that the same vuln.OSV.ID has vuln.CallSink != 0 + // for one symbol, but vuln.CallSink == 0 for a different one, so + // we need to filter out ones that have been called. + called := map[string]bool{} + for _, vuln := range vulns { + if vuln.CallSink != 0 { + called[vuln.OSV.ID] = true + } + } + + modToIDs := map[string]map[string]*osv.Entry{} + for _, vuln := range vulns { + if !called[vuln.OSV.ID] { + if _, ok := modToIDs[vuln.ModPath]; !ok { + modToIDs[vuln.ModPath] = map[string]*osv.Entry{} + } + // keep only one vuln.OSV instance for the same ID. + modToIDs[vuln.ModPath][vuln.OSV.ID] = vuln.OSV + } + } + output := map[string][]*osv.Entry{} + for m, vulnSet := range modToIDs { + var vulns []*osv.Entry + for _, vuln := range vulnSet { + vulns = append(vulns, vuln) + } + sort.Slice(vulns, func(i, j int) bool { return vulns[i].ID < vulns[j].ID }) + output[m] = vulns + } + return output +} + +func fixed(v *osv.Entry) string { + lf := gvc.LatestFixed(v.Affected) + if lf != "" && lf[0] != 'v' { + lf = "v" + lf + } + return lf +} + +func toVulns(ci *gvc.CallInfo, unaffectedMods map[string][]*osv.Entry) ([]Vuln, error) { var vulns []Vuln for _, vg := range ci.VulnGroups { v0 := vg[0] - lf := gvc.LatestFixed(v0.OSV.Affected) - if lf != "" && lf[0] != 'v' { - lf = "v" + lf - } vuln := Vuln{ ID: v0.OSV.ID, PkgPath: v0.PkgPath, CurrentVersion: ci.ModuleVersions[v0.ModPath], - FixedVersion: lf, + FixedVersion: fixed(v0.OSV), Details: v0.OSV.Details, Aliases: v0.OSV.Aliases, @@ -114,10 +177,58 @@ func toVulns(ci *gvc.CallInfo) ([]Vuln, error) { for _, v := range vg { if css := ci.CallStacks[v]; len(css) > 0 { vuln.CallStacks = append(vuln.CallStacks, toCallStack(css[0])) - vuln.CallStackSummaries = append(vuln.CallStackSummaries, gvc.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath)) + // TODO(hyangah): https://go-review.googlesource.com/c/vuln/+/425183 added position info + // in the summary but we don't need the info. Allow SummarizeCallStack to skip it optionally. + sum := trimPosPrefix(gvc.SummarizeCallStack(css[0], ci.TopPackages, v.PkgPath)) + vuln.CallStackSummaries = append(vuln.CallStackSummaries, sum) } } vulns = append(vulns, vuln) } + for m, vg := range unaffectedMods { + for _, v0 := range vg { + vuln := Vuln{ + ID: v0.ID, + Details: v0.Details, + Aliases: v0.Aliases, + ModPath: m, + URL: href(v0), + CurrentVersion: "", + FixedVersion: fixed(v0), + } + vulns = append(vulns, vuln) + } + } return vulns, nil } + +func trimPosPrefix(summary string) string { + _, after, found := strings.Cut(summary, ": ") + if !found { + return summary + } + return after +} + +// GoVersionForVulnTest is an internal environment variable used in gopls +// testing to examine govulncheck behavior with a go version different +// than what `go version` returns in the system. +const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION" + +func init() { + Main = func(cfg packages.Config, patterns ...string) { + // never return + err := gvcapi.Main(gvcapi.Config{ + AnalysisType: "source", + OutputType: "summary", + Patterns: patterns, + SourceLoadConfig: &cfg, + GoVersion: os.Getenv(GoVersionForVulnTest), + }) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) + } +} diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go index f689ab96722..a9ab2c0a731 100644 --- a/gopls/internal/vulncheck/command_test.go +++ b/gopls/internal/vulncheck/command_test.go @@ -14,22 +14,32 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/vuln/client" - "golang.org/x/vuln/osv" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/fake" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/gopls/internal/vulncheck/vulntest" ) func TestCmd_Run(t *testing.T) { runTest(t, workspace1, proxy1, func(ctx context.Context, snapshot source.Snapshot) { - cmd := &cmd{Client: testClient1} + db, err := vulntest.NewDatabase(ctx, []byte(vulnsData)) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + cli, err := vulntest.NewClient(db) + if err != nil { + t.Fatal(err) + } + + cmd := &cmd{Client: cli} cfg := packagesCfg(ctx, snapshot) result, err := cmd.Run(ctx, cfg, "./...") if err != nil { @@ -42,11 +52,19 @@ func TestCmd_Run(t *testing.T) { for _, v := range result { got = append(got, toReport(v)) } + // drop the workspace root directory path included in the summary. + cwd := cfg.Dir + for _, g := range got { + for i, summary := range g.CallStackSummaries { + g.CallStackSummaries[i] = strings.ReplaceAll(summary, cwd, ".") + } + } var want = []report{ { Vuln: Vuln{ ID: "GO-2022-01", + Details: "Something.\n", Symbol: "VulnData.Vuln1", PkgPath: "golang.org/amod/avuln", ModPath: "golang.org/amod", @@ -81,6 +99,15 @@ func TestCmd_Run(t *testing.T) { "golang.org/bmod/bvuln.Vuln (bvuln.go:2)\n", }, }, + { + Vuln: Vuln{ + ID: "GO-2022-03", + Details: "unaffecting vulnerability.\n", + ModPath: "golang.org/amod", + URL: "https://pkg.go.dev/vuln/GO-2022-03", + FixedVersion: "v1.0.4", + }, + }, } // sort reports for stability before comparison. for _, rpts := range [][]report{got, want} { @@ -207,49 +234,46 @@ func Vuln() { } ` -// testClient contains the following test vulnerabilities -// -// golang.org/amod/avuln.{VulnData.Vuln1, vulnData.Vuln2} -// golang.org/bmod/bvuln.{Vuln} -var testClient1 = &mockClient{ - ret: map[string][]*osv.Entry{ - "golang.org/amod": { - { - ID: "GO-2022-01", - References: []osv.Reference{ - { - Type: "href", - URL: "pkg.go.dev/vuln/GO-2022-01", - }, - }, - Affected: []osv.Affected{{ - Package: osv.Package{Name: "golang.org/amod/avuln"}, - Ranges: osv.Affects{{Type: osv.TypeSemver, Events: []osv.RangeEvent{{Introduced: "1.0.0"}, {Fixed: "1.0.4"}, {Introduced: "1.1.2"}}}}, - EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"VulnData.Vuln1", "VulnData.Vuln2"}}, - }}, - }, - }, - "golang.org/bmod": { - { - ID: "GO-2022-02", - Affected: []osv.Affected{{ - Package: osv.Package{Name: "golang.org/bmod/bvuln"}, - Ranges: osv.Affects{{Type: osv.TypeSemver}}, - EcosystemSpecific: osv.EcosystemSpecific{Symbols: []string{"Vuln"}}, - }}, - }, - }, - }, -} - -type mockClient struct { - client.Client - ret map[string][]*osv.Entry -} - -func (mc *mockClient) GetByModule(ctx context.Context, a string) ([]*osv.Entry, error) { - return mc.ret[a], nil -} +const vulnsData = ` +-- GO-2022-01.yaml -- +modules: + - module: golang.org/amod + versions: + - introduced: 1.0.0 + - fixed: 1.0.4 + - introduced: 1.1.2 + packages: + - package: golang.org/amod/avuln + symbols: + - VulnData.Vuln1 + - VulnData.Vuln2 +description: | + Something. +references: + - href: pkg.go.dev/vuln/GO-2022-01 + +-- GO-2022-03.yaml -- +modules: + - module: golang.org/amod + versions: + - introduced: 1.0.0 + - fixed: 1.0.4 + - introduced: 1.1.2 + packages: + - package: golang.org/amod/avuln + symbols: + - nonExisting +description: | + unaffecting vulnerability. + +-- GO-2022-02.yaml -- +modules: + - module: golang.org/bmod + packages: + - package: golang.org/bmod/bvuln + symbols: + - Vuln +` func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Context, source.Snapshot)) { ws, err := fake.NewSandbox(&fake.SandboxConfig{ @@ -269,7 +293,7 @@ func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Co t.Fatal(err) } - cache := cache.New(nil) + cache := cache.New(nil, nil, nil) session := cache.NewSession(ctx) options := source.DefaultOptions().Clone() tests.DefaultOptions(options) @@ -285,8 +309,13 @@ func runTest(t *testing.T, workspaceData, proxyData string, test func(context.Co if err != nil { t.Fatal(err) } - defer release() - defer view.Shutdown(ctx) + + defer func() { + // The snapshot must be released before calling view.Shutdown, to avoid a + // deadlock. + release() + view.Shutdown(ctx) + }() test(ctx, snapshot) } diff --git a/gopls/internal/vulncheck/util.go b/gopls/internal/vulncheck/util.go index c329461894e..d21d30517a4 100644 --- a/gopls/internal/vulncheck/util.go +++ b/gopls/internal/vulncheck/util.go @@ -8,11 +8,14 @@ package vulncheck import ( + "bytes" "fmt" "go/token" + "os" + "os/exec" gvc "golang.org/x/tools/gopls/internal/govulncheck" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/vuln/osv" "golang.org/x/vuln/vulncheck" ) @@ -80,3 +83,16 @@ func posToPosition(pos *token.Position) (p protocol.Position) { } return p } + +func goVersion() string { + if v := os.Getenv("GOVERSION"); v != "" { + // Unlikely to happen in practice, mostly used for testing. + return v + } + out, err := exec.Command("go", "env", "GOVERSION").Output() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to determine go version; skipping stdlib scanning: %v\n", err) + return "" + } + return string(bytes.TrimSpace(out)) +} diff --git a/gopls/internal/vulncheck/vulncheck.go b/gopls/internal/vulncheck/vulncheck.go index 2c4d0d2978d..7167ec1c266 100644 --- a/gopls/internal/vulncheck/vulncheck.go +++ b/gopls/internal/vulncheck/vulncheck.go @@ -10,14 +10,13 @@ package vulncheck import ( "context" - "errors" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/command" + "golang.org/x/tools/gopls/internal/lsp/command" ) // Govulncheck runs the in-process govulncheck implementation. // With go1.18+, this is swapped with the real implementation. -var Govulncheck = func(ctx context.Context, cfg *packages.Config, args command.VulncheckArgs) (res command.VulncheckResult, _ error) { - return res, errors.New("not implemented") -} +var Govulncheck func(ctx context.Context, cfg *packages.Config, patterns string) (res command.VulncheckResult, _ error) = nil + +var Main func(cfg packages.Config, patterns ...string) = nil diff --git a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go new file mode 100644 index 00000000000..511a47e1ba9 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/db.go @@ -0,0 +1,303 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +// Package vulntest provides helpers for vulncheck functionality testing. +package vulntest + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/txtar" + "golang.org/x/vuln/client" + "golang.org/x/vuln/osv" +) + +// NewDatabase returns a read-only DB containing the provided +// txtar-format collection of vulnerability reports. +// Each vulnerability report is a YAML file whose format +// is defined in golang.org/x/vulndb/doc/format.md. +// A report file name must have the id as its base name, +// and have .yaml as its extension. +// +// db, err := NewDatabase(ctx, reports) +// ... +// defer db.Clean() +// client, err := NewClient(db) +// ... +// +// The returned DB's Clean method must be called to clean up the +// generated database. +func NewDatabase(ctx context.Context, txtarReports []byte) (*DB, error) { + disk, err := ioutil.TempDir("", "vulndb-test") + if err != nil { + return nil, err + } + if err := generateDB(ctx, txtarReports, disk, false); err != nil { + os.RemoveAll(disk) + return nil, err + } + + return &DB{disk: disk}, nil +} + +// DB is a read-only vulnerability database on disk. +// Users can use this database with golang.org/x/vuln APIs +// by setting the `VULNDB“ environment variable. +type DB struct { + disk string +} + +// URI returns the file URI that can be used for VULNDB environment +// variable. +func (db *DB) URI() string { + u := span.URIFromPath(db.disk) + return string(u) +} + +// Clean deletes the database. +func (db *DB) Clean() error { + return os.RemoveAll(db.disk) +} + +// NewClient returns a vuln DB client that works with the given DB. +func NewClient(db *DB) (client.Client, error) { + return client.NewClient([]string{db.URI()}, client.Options{}) +} + +// +// The following was selectively copied from golang.org/x/vulndb/internal/database +// + +const ( + dbURL = "https://pkg.go.dev/vuln/" + + // idDirectory is the name of the directory that contains entries + // listed by their IDs. + idDirectory = "ID" + + // stdFileName is the name of the .json file in the vulndb repo + // that will contain info on standard library vulnerabilities. + stdFileName = "stdlib" + + // toolchainFileName is the name of the .json file in the vulndb repo + // that will contain info on toolchain (cmd/...) vulnerabilities. + toolchainFileName = "toolchain" + + // cmdModule is the name of the module containing Go toolchain + // binaries. + cmdModule = "cmd" + + // stdModule is the name of the module containing Go std packages. + stdModule = "std" +) + +// generateDB generates the file-based vuln DB in the directory jsonDir. +func generateDB(ctx context.Context, txtarData []byte, jsonDir string, indent bool) error { + archive := txtar.Parse(txtarData) + + jsonVulns, entries, err := generateEntries(ctx, archive) + if err != nil { + return err + } + + index := make(client.DBIndex, len(jsonVulns)) + for modulePath, vulns := range jsonVulns { + epath, err := client.EscapeModulePath(modulePath) + if err != nil { + return err + } + if err := writeVulns(filepath.Join(jsonDir, epath), vulns, indent); err != nil { + return err + } + for _, v := range vulns { + if v.Modified.After(index[modulePath]) { + index[modulePath] = v.Modified + } + } + } + if err := writeJSON(filepath.Join(jsonDir, "index.json"), index, indent); err != nil { + return err + } + if err := writeAliasIndex(jsonDir, entries, indent); err != nil { + return err + } + return writeEntriesByID(filepath.Join(jsonDir, idDirectory), entries, indent) +} + +func generateEntries(_ context.Context, archive *txtar.Archive) (map[string][]osv.Entry, []osv.Entry, error) { + now := time.Now() + jsonVulns := map[string][]osv.Entry{} + var entries []osv.Entry + for _, f := range archive.Files { + if !strings.HasSuffix(f.Name, ".yaml") { + continue + } + r, err := readReport(bytes.NewReader(f.Data)) + if err != nil { + return nil, nil, err + } + name := strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name)) + linkName := fmt.Sprintf("%s%s", dbURL, name) + entry, modulePaths := generateOSVEntry(name, linkName, now, *r) + for _, modulePath := range modulePaths { + jsonVulns[modulePath] = append(jsonVulns[modulePath], entry) + } + entries = append(entries, entry) + } + return jsonVulns, entries, nil +} + +func writeVulns(outPath string, vulns []osv.Entry, indent bool) error { + if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { + return fmt.Errorf("failed to create directory %q: %s", filepath.Dir(outPath), err) + } + return writeJSON(outPath+".json", vulns, indent) +} + +func writeEntriesByID(idDir string, entries []osv.Entry, indent bool) error { + // Write a directory containing entries by ID. + if err := os.MkdirAll(idDir, 0755); err != nil { + return fmt.Errorf("failed to create directory %q: %v", idDir, err) + } + var idIndex []string + for _, e := range entries { + outPath := filepath.Join(idDir, e.ID+".json") + if err := writeJSON(outPath, e, indent); err != nil { + return err + } + idIndex = append(idIndex, e.ID) + } + // Write an index.json in the ID directory with a list of all the IDs. + return writeJSON(filepath.Join(idDir, "index.json"), idIndex, indent) +} + +// Write a JSON file containing a map from alias to GO IDs. +func writeAliasIndex(dir string, entries []osv.Entry, indent bool) error { + aliasToGoIDs := map[string][]string{} + for _, e := range entries { + for _, a := range e.Aliases { + aliasToGoIDs[a] = append(aliasToGoIDs[a], e.ID) + } + } + return writeJSON(filepath.Join(dir, "aliases.json"), aliasToGoIDs, indent) +} + +func writeJSON(filename string, value any, indent bool) (err error) { + j, err := jsonMarshal(value, indent) + if err != nil { + return err + } + return os.WriteFile(filename, j, 0644) +} + +func jsonMarshal(v any, indent bool) ([]byte, error) { + if indent { + return json.MarshalIndent(v, "", " ") + } + return json.Marshal(v) +} + +// generateOSVEntry create an osv.Entry for a report. In addition to the report, it +// takes the ID for the vuln and a URL that will point to the entry in the vuln DB. +// It returns the osv.Entry and a list of module paths that the vuln affects. +func generateOSVEntry(id, url string, lastModified time.Time, r Report) (osv.Entry, []string) { + entry := osv.Entry{ + ID: id, + Published: r.Published, + Modified: lastModified, + Withdrawn: r.Withdrawn, + Details: r.Description, + } + + moduleMap := make(map[string]bool) + for _, m := range r.Modules { + switch m.Module { + case stdModule: + moduleMap[stdFileName] = true + case cmdModule: + moduleMap[toolchainFileName] = true + default: + moduleMap[m.Module] = true + } + entry.Affected = append(entry.Affected, generateAffected(m, url)) + } + for _, ref := range r.References { + entry.References = append(entry.References, osv.Reference{ + Type: string(ref.Type), + URL: ref.URL, + }) + } + + var modulePaths []string + for module := range moduleMap { + modulePaths = append(modulePaths, module) + } + // TODO: handle missing fields - Aliases + + return entry, modulePaths +} + +func generateAffectedRanges(versions []VersionRange) osv.Affects { + a := osv.AffectsRange{Type: osv.TypeSemver} + if len(versions) == 0 || versions[0].Introduced == "" { + a.Events = append(a.Events, osv.RangeEvent{Introduced: "0"}) + } + for _, v := range versions { + if v.Introduced != "" { + a.Events = append(a.Events, osv.RangeEvent{Introduced: v.Introduced.Canonical()}) + } + if v.Fixed != "" { + a.Events = append(a.Events, osv.RangeEvent{Fixed: v.Fixed.Canonical()}) + } + } + return osv.Affects{a} +} + +func generateImports(m *Module) (imps []osv.EcosystemSpecificImport) { + for _, p := range m.Packages { + syms := append([]string{}, p.Symbols...) + syms = append(syms, p.DerivedSymbols...) + sort.Strings(syms) + imps = append(imps, osv.EcosystemSpecificImport{ + Path: p.Package, + GOOS: p.GOOS, + GOARCH: p.GOARCH, + Symbols: syms, + }) + } + return imps +} +func generateAffected(m *Module, url string) osv.Affected { + name := m.Module + switch name { + case stdModule: + name = "stdlib" + case cmdModule: + name = "toolchain" + } + return osv.Affected{ + Package: osv.Package{ + Name: name, + Ecosystem: osv.GoEcosystem, + }, + Ranges: generateAffectedRanges(m.Versions), + DatabaseSpecific: osv.DatabaseSpecific{URL: url}, + EcosystemSpecific: osv.EcosystemSpecific{ + Imports: generateImports(m), + }, + } +} diff --git a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulncheck/vulntest/db_test.go new file mode 100644 index 00000000000..7d939421c94 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/db_test.go @@ -0,0 +1,61 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import ( + "context" + "encoding/json" + "testing" +) + +func TestNewDatabase(t *testing.T) { + ctx := context.Background() + in := []byte(` +-- GO-2020-0001.yaml -- +modules: + - module: github.com/gin-gonic/gin + versions: + - fixed: 1.6.0 + packages: + - package: github.com/gin-gonic/gin + symbols: + - defaultLogFormatter +description: | + Something. +published: 2021-04-14T20:04:52Z +references: + - fix: https://github.com/gin-gonic/gin/pull/2237 +`) + + db, err := NewDatabase(ctx, in) + if err != nil { + t.Fatal(err) + } + defer db.Clean() + + cli, err := NewClient(db) + if err != nil { + t.Fatal(err) + } + got, err := cli.GetByID(ctx, "GO-2020-0001") + if err != nil { + t.Fatal(err) + } + if got.ID != "GO-2020-0001" { + m, _ := json.Marshal(got) + t.Errorf("got %s\nwant GO-2020-0001 entry", m) + } + gotAll, err := cli.GetByModule(ctx, "github.com/gin-gonic/gin") + if err != nil { + t.Fatal(err) + } + if len(gotAll) != 1 || gotAll[0].ID != "GO-2020-0001" { + m, _ := json.Marshal(got) + t.Errorf("got %s\nwant GO-2020-0001 entry", m) + } +} diff --git a/gopls/internal/vulncheck/vulntest/report.go b/gopls/internal/vulncheck/vulntest/report.go new file mode 100644 index 00000000000..e5595e8ba06 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/report.go @@ -0,0 +1,176 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import ( + "fmt" + "io" + "os" + "strings" + "time" + + "golang.org/x/mod/semver" + "gopkg.in/yaml.v3" +) + +// +// The following was selectively copied from golang.org/x/vulndb/internal/report +// + +// readReport reads a Report in YAML format. +func readReport(in io.Reader) (*Report, error) { + d := yaml.NewDecoder(in) + // Require that all fields in the file are in the struct. + // This corresponds to v2's UnmarshalStrict. + d.KnownFields(true) + var r Report + if err := d.Decode(&r); err != nil { + return nil, fmt.Errorf("yaml.Decode: %v", err) + } + return &r, nil +} + +// Report represents a vulnerability report in the vulndb. +// Remember to update doc/format.md when this structure changes. +type Report struct { + Modules []*Module `yaml:",omitempty"` + + // Description is the CVE description from an existing CVE. If we are + // assigning a CVE ID ourselves, use CVEMetadata.Description instead. + Description string `yaml:",omitempty"` + Published time.Time `yaml:",omitempty"` + Withdrawn *time.Time `yaml:",omitempty"` + + References []*Reference `yaml:",omitempty"` +} + +// Write writes r to filename in YAML format. +func (r *Report) Write(filename string) (err error) { + f, err := os.Create(filename) + if err != nil { + return err + } + err = r.encode(f) + err2 := f.Close() + if err == nil { + err = err2 + } + return err +} + +// ToString encodes r to a YAML string. +func (r *Report) ToString() (string, error) { + var b strings.Builder + if err := r.encode(&b); err != nil { + return "", err + } + return b.String(), nil +} + +func (r *Report) encode(w io.Writer) error { + e := yaml.NewEncoder(w) + defer e.Close() + e.SetIndent(4) + return e.Encode(r) +} + +type VersionRange struct { + Introduced Version `yaml:"introduced,omitempty"` + Fixed Version `yaml:"fixed,omitempty"` +} + +type Module struct { + Module string `yaml:",omitempty"` + Versions []VersionRange `yaml:",omitempty"` + Packages []*Package `yaml:",omitempty"` +} + +type Package struct { + Package string `yaml:",omitempty"` + GOOS []string `yaml:"goos,omitempty"` + GOARCH []string `yaml:"goarch,omitempty"` + // Symbols originally identified as vulnerable. + Symbols []string `yaml:",omitempty"` + // Additional vulnerable symbols, computed from Symbols via static analysis + // or other technique. + DerivedSymbols []string `yaml:"derived_symbols,omitempty"` +} + +// Version is an SemVer 2.0.0 semantic version with no leading "v" prefix, +// as used by OSV. +type Version string + +// V returns the version with a "v" prefix. +func (v Version) V() string { + return "v" + string(v) +} + +// IsValid reports whether v is a valid semantic version string. +func (v Version) IsValid() bool { + return semver.IsValid(v.V()) +} + +// Before reports whether v < v2. +func (v Version) Before(v2 Version) bool { + return semver.Compare(v.V(), v2.V()) < 0 +} + +// Canonical returns the canonical formatting of the version. +func (v Version) Canonical() string { + return strings.TrimPrefix(semver.Canonical(v.V()), "v") +} + +// Reference type is a reference (link) type. +type ReferenceType string + +const ( + ReferenceTypeAdvisory = ReferenceType("ADVISORY") + ReferenceTypeArticle = ReferenceType("ARTICLE") + ReferenceTypeReport = ReferenceType("REPORT") + ReferenceTypeFix = ReferenceType("FIX") + ReferenceTypePackage = ReferenceType("PACKAGE") + ReferenceTypeEvidence = ReferenceType("EVIDENCE") + ReferenceTypeWeb = ReferenceType("WEB") +) + +// ReferenceTypes is the set of reference types defined in OSV. +var ReferenceTypes = []ReferenceType{ + ReferenceTypeAdvisory, + ReferenceTypeArticle, + ReferenceTypeReport, + ReferenceTypeFix, + ReferenceTypePackage, + ReferenceTypeEvidence, + ReferenceTypeWeb, +} + +// A Reference is a link to some external resource. +// +// For ease of typing, References are represented in the YAML as a +// single-element mapping of type to URL. +type Reference struct { + Type ReferenceType `json:"type,omitempty"` + URL string `json:"url,omitempty"` +} + +func (r *Reference) MarshalYAML() (interface{}, error) { + return map[string]string{ + strings.ToLower(string(r.Type)): r.URL, + }, nil +} + +func (r *Reference) UnmarshalYAML(n *yaml.Node) (err error) { + if n.Kind != yaml.MappingNode || len(n.Content) != 2 || n.Content[0].Kind != yaml.ScalarNode || n.Content[1].Kind != yaml.ScalarNode { + return &yaml.TypeError{Errors: []string{ + fmt.Sprintf("line %d: report.Reference must contain a mapping with one value", n.Line), + }} + } + r.Type = ReferenceType(strings.ToUpper(n.Content[0].Value)) + r.URL = n.Content[1].Value + return nil +} diff --git a/gopls/internal/vulncheck/vulntest/report_test.go b/gopls/internal/vulncheck/vulntest/report_test.go new file mode 100644 index 00000000000..c42dae805fa --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/report_test.go @@ -0,0 +1,52 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func readAll(t *testing.T, filename string) io.Reader { + d, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + return bytes.NewReader(d) +} + +func TestRoundTrip(t *testing.T) { + // A report shouldn't change after being read and then written. + in := filepath.Join("testdata", "report.yaml") + r, err := readReport(readAll(t, in)) + if err != nil { + t.Fatal(err) + } + out := filepath.Join(t.TempDir(), "report.yaml") + if err := r.Write(out); err != nil { + t.Fatal(err) + } + + want, err := os.ReadFile(in) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch (-want, +got):\n%s", diff) + } +} diff --git a/gopls/internal/vulncheck/vulntest/stdlib.go b/gopls/internal/vulncheck/vulntest/stdlib.go new file mode 100644 index 00000000000..9bf4d4ef0d4 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/stdlib.go @@ -0,0 +1,26 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import ( + "strings" + + "golang.org/x/mod/module" +) + +// maybeStdlib reports whether the given import path could be part of the Go +// standard library, by reporting whether the first component lacks a '.'. +func maybeStdlib(path string) bool { + if err := module.CheckImportPath(path); err != nil { + return false + } + if i := strings.IndexByte(path, '/'); i != -1 { + path = path[:i] + } + return !strings.Contains(path, ".") +} diff --git a/gopls/internal/vulncheck/vulntest/stdlib_test.go b/gopls/internal/vulncheck/vulntest/stdlib_test.go new file mode 100644 index 00000000000..8f893f3ec42 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/stdlib_test.go @@ -0,0 +1,27 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 +// +build go1.18 + +package vulntest + +import "testing" + +func TestMaybeStdlib(t *testing.T) { + for _, test := range []struct { + in string + want bool + }{ + {"", false}, + {"math/crypto", true}, + {"github.com/pkg/errors", false}, + {"Path is unknown", false}, + } { + got := maybeStdlib(test.in) + if got != test.want { + t.Errorf("%q: got %t, want %t", test.in, got, test.want) + } + } +} diff --git a/gopls/internal/vulncheck/vulntest/testdata/report.yaml b/gopls/internal/vulncheck/vulntest/testdata/report.yaml new file mode 100644 index 00000000000..48384b543b2 --- /dev/null +++ b/gopls/internal/vulncheck/vulntest/testdata/report.yaml @@ -0,0 +1,15 @@ +modules: + - module: github.com/gin-gonic/gin + versions: + - fixed: 1.6.0 + packages: + - package: github.com/gin-gonic/gin + symbols: + - defaultLogFormatter +description: | + The default Formatter for the Logger middleware (LoggerConfig.Formatter), + which is included in the Default engine, allows attackers to inject arbitrary + log entries by manipulating the request path. +references: + - fix: https://github.com/gin-gonic/gin/pull/1234 + - fix: https://github.com/gin-gonic/gin/commit/abcdefg diff --git a/gopls/main.go b/gopls/main.go index f73eabf5767..837a01d40b5 100644 --- a/gopls/main.go +++ b/gopls/main.go @@ -17,7 +17,7 @@ import ( "os" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/gopls/internal/lsp/cmd" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/release/release.go b/gopls/release/release.go index 173909122b3..fb456b6aa0e 100644 --- a/gopls/release/release.go +++ b/gopls/release/release.go @@ -135,7 +135,7 @@ func validateHardcodedVersion(wd string, version string) error { Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes, - }, "golang.org/x/tools/internal/lsp/debug") + }, "golang.org/x/tools/gopls/internal/lsp/debug") if err != nil { return err } diff --git a/gopls/test/debug/debug_test.go b/gopls/test/debug/debug_test.go index 4d680eebbbe..9d5d6f0f12b 100644 --- a/gopls/test/debug/debug_test.go +++ b/gopls/test/debug/debug_test.go @@ -21,10 +21,10 @@ import ( "github.com/jba/templatecheck" "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/gopls/internal/lsp/cache" + "golang.org/x/tools/gopls/internal/lsp/debug" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/span" ) type tdata struct { @@ -90,7 +90,7 @@ func TestTemplates(t *testing.T) { cfg := &packages.Config{ Mode: packages.NeedTypesInfo | packages.LoadAllSyntax, // figure out what's necessary PJW } - pkgs, err := packages.Load(cfg, "golang.org/x/tools/internal/lsp/debug") + pkgs, err := packages.Load(cfg, "golang.org/x/tools/gopls/internal/lsp/debug") if err != nil { t.Fatal(err) } diff --git a/gopls/test/gopls_test.go b/gopls/test/gopls_test.go index 6282224abb5..95534d44327 100644 --- a/gopls/test/gopls_test.go +++ b/gopls/test/gopls_test.go @@ -9,21 +9,29 @@ import ( "testing" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/internal/lsp/bug" - cmdtest "golang.org/x/tools/internal/lsp/cmd/test" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/lsp/tests" + cmdtest "golang.org/x/tools/gopls/internal/lsp/cmd/test" + "golang.org/x/tools/gopls/internal/lsp/source" + "golang.org/x/tools/gopls/internal/lsp/tests" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { bug.PanicOnBugs = true testenv.ExitIfSmallMachine() + + // Set the global exporter to nil so that we don't log to stderr. This avoids + // a lot of misleading noise in test output. + // + // See also ../internal/lsp/lsp_test.go. + event.SetExporter(nil) + os.Exit(m.Run()) } func TestCommandLine(t *testing.T) { - cmdtest.TestCommandLine(t, "../../internal/lsp/testdata", commandLineOptions) + cmdtest.TestCommandLine(t, "../internal/lsp/testdata", commandLineOptions) } func commandLineOptions(options *source.Options) { diff --git a/gopls/test/json_test.go b/gopls/test/json_test.go index 5ea5b343450..993af3095a1 100644 --- a/gopls/test/json_test.go +++ b/gopls/test/json_test.go @@ -12,7 +12,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/protocol" ) // verify that type errors in Initialize lsp messages don't cause @@ -99,7 +99,7 @@ func allDeltas(t *testing.T, v [][]int, repls ...string) { } func tryChange(start, end int, repl string) error { - var p, q protocol.InitializeParams + var p, q protocol.ParamInitialize mod := input[:start] + repl + input[end:] excerpt := func() (string, string) { a := start - 5 diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index 3f1e573342f..6fceef5e720 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -12,11 +12,10 @@ import ( "go/token" "go/types" "strconv" - - "golang.org/x/tools/internal/lsp/fuzzy" ) -// Flag to gate diagnostics for fuzz tests in 1.18. +// DiagnoseFuzzTests controls whether the 'tests' analyzer diagnoses fuzz tests +// in Go 1.18+. var DiagnoseFuzzTests bool = false var ( @@ -80,6 +79,9 @@ func IsZeroValue(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. func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { switch t := typ.(type) { case *types.Basic: @@ -309,19 +311,21 @@ func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) { }) } -// FindMatchingIdents finds all identifiers in 'node' that match any of the given types. +// MatchingIdents finds the names of all identifiers in 'node' that match any of the given types. // 'pos' represents the position at which the identifiers may be inserted. 'pos' must be within // the scope of each of identifier we select. Otherwise, we will insert a variable at 'pos' that // is unrecognized. -func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]*ast.Ident { - matches := map[types.Type][]*ast.Ident{} +func MatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]string { + // Initialize matches to contain the variable types we are searching for. + matches := make(map[types.Type][]string) for _, typ := range typs { if typ == nil { - continue + continue // TODO(adonovan): is this reachable? } - matches[typ] = []*ast.Ident{} + matches[typ] = nil // create entry } + seen := map[types.Object]struct{}{} ast.Inspect(node, func(n ast.Node) bool { if n == nil { @@ -333,8 +337,7 @@ func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *t // // x := fakeStruct{f0: x} // - assignment, ok := n.(*ast.AssignStmt) - if ok && pos > assignment.Pos() && pos <= assignment.End() { + if assign, ok := n.(*ast.AssignStmt); ok && pos > assign.Pos() && pos <= assign.End() { return false } if n.End() > pos { @@ -367,17 +370,17 @@ func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *t return true } // The object must match one of the types that we are searching for. - if idents, ok := matches[obj.Type()]; ok { - matches[obj.Type()] = append(idents, ast.NewIdent(ident.Name)) - } - // If the object type does not exactly match any of the target types, greedily - // find the first target type that the object type can satisfy. - for typ := range matches { - if obj.Type() == typ { - continue - } - if equivalentTypes(obj.Type(), typ) { - matches[typ] = append(matches[typ], ast.NewIdent(ident.Name)) + // TODO(adonovan): opt: use typeutil.Map? + if names, ok := matches[obj.Type()]; ok { + matches[obj.Type()] = append(names, ident.Name) + } else { + // If the object type does not exactly match + // any of the target types, greedily find the first + // target type that the object type can satisfy. + for typ := range matches { + if equivalentTypes(obj.Type(), typ) { + matches[typ] = append(matches[typ], ident.Name) + } } } return true @@ -386,7 +389,7 @@ func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *t } func equivalentTypes(want, got types.Type) bool { - if want == got || types.Identical(want, got) { + if types.Identical(want, got) { return true } // Code segment to help check for untyped equality from (golang/go#32146). @@ -397,30 +400,3 @@ func equivalentTypes(want, got types.Type) bool { } return types.AssignableTo(want, got) } - -// FindBestMatch employs fuzzy matching to evaluate the similarity of each given identifier to the -// given pattern. We return the identifier whose name is most similar to the pattern. -func FindBestMatch(pattern string, idents []*ast.Ident) ast.Expr { - fuzz := fuzzy.NewMatcher(pattern) - var bestFuzz ast.Expr - highScore := float32(0) // minimum score is 0 (no match) - for _, ident := range idents { - // TODO: Improve scoring algorithm. - score := fuzz.Score(ident.Name) - if score > highScore { - highScore = score - bestFuzz = ident - } else if score == 0 { - // Order matters in the fuzzy matching algorithm. If we find no match - // when matching the target to the identifier, try matching the identifier - // to the target. - revFuzz := fuzzy.NewMatcher(ident.Name) - revScore := revFuzz.Score(pattern) - if revScore > highScore { - highScore = revScore - bestFuzz = ident - } - } - } - return bestFuzz -} diff --git a/internal/lsp/bug/bug.go b/internal/bug/bug.go similarity index 100% rename from internal/lsp/bug/bug.go rename to internal/bug/bug.go diff --git a/internal/lsp/bug/bug_test.go b/internal/bug/bug_test.go similarity index 100% rename from internal/lsp/bug/bug_test.go rename to internal/bug/bug_test.go diff --git a/internal/diff/diff.go b/internal/diff/diff.go new file mode 100644 index 00000000000..7b08ad57c82 --- /dev/null +++ b/internal/diff/diff.go @@ -0,0 +1,161 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package diff computes differences between text files or strings. +package diff + +import ( + "fmt" + "sort" + "strings" +) + +// An Edit describes the replacement of a portion of a text file. +type Edit struct { + Start, End int // byte offsets of the region to replace + New string // the replacement +} + +// Apply applies a sequence of edits to the src buffer and returns the +// result. Edits are applied in order of start offset; edits with the +// same start offset are applied in they order they were provided. +// +// Apply returns an error if any edit is out of bounds, +// or if any pair of edits is overlapping. +func Apply(src string, edits []Edit) (string, error) { + edits, size, err := validate(src, edits) + if err != nil { + return "", err + } + + // Apply edits. + out := make([]byte, 0, size) + lastEnd := 0 + for _, edit := range edits { + if lastEnd < edit.Start { + out = append(out, src[lastEnd:edit.Start]...) + } + out = append(out, edit.New...) + lastEnd = edit.End + } + out = append(out, src[lastEnd:]...) + + if len(out) != size { + panic("wrong size") + } + + return string(out), nil +} + +// validate checks that edits are consistent with src, +// and returns the size of the patched output. +// It may return a different slice. +func validate(src string, edits []Edit) ([]Edit, int, error) { + if !sort.IsSorted(editsSort(edits)) { + edits = append([]Edit(nil), edits...) + SortEdits(edits) + } + + // Check validity of edits and compute final size. + size := len(src) + lastEnd := 0 + for _, edit := range edits { + if !(0 <= edit.Start && edit.Start <= edit.End && edit.End <= len(src)) { + return nil, 0, fmt.Errorf("diff has out-of-bounds edits") + } + if edit.Start < lastEnd { + return nil, 0, fmt.Errorf("diff has overlapping edits") + } + size += len(edit.New) + edit.Start - edit.End + lastEnd = edit.End + } + + return edits, size, nil +} + +// SortEdits orders a slice of Edits by (start, end) offset. +// This ordering puts insertions (end = start) before deletions +// (end > start) at the same point, but uses a stable sort to preserve +// the order of multiple insertions at the same point. +// (Apply detects multiple deletions at the same point as an error.) +func SortEdits(edits []Edit) { + sort.Stable(editsSort(edits)) +} + +type editsSort []Edit + +func (a editsSort) Len() int { return len(a) } +func (a editsSort) Less(i, j int) bool { + if cmp := a[i].Start - a[j].Start; cmp != 0 { + return cmp < 0 + } + return a[i].End < a[j].End +} +func (a editsSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +// lineEdits expands and merges a sequence of edits so that each +// resulting edit replaces one or more complete lines. +// See ApplyEdits for preconditions. +func lineEdits(src string, edits []Edit) ([]Edit, error) { + edits, _, err := validate(src, edits) + if err != nil { + return nil, err + } + + // Do all edits begin and end at the start of a line? + // TODO(adonovan): opt: is this fast path necessary? + // (Also, it complicates the result ownership.) + for _, edit := range edits { + if edit.Start >= len(src) || // insertion at EOF + edit.Start > 0 && src[edit.Start-1] != '\n' || // not at line start + edit.End > 0 && src[edit.End-1] != '\n' { // not at line start + goto expand + } + } + return edits, nil // aligned + +expand: + expanded := make([]Edit, 0, len(edits)) // a guess + prev := edits[0] + // TODO(adonovan): opt: start from the first misaligned edit. + // TODO(adonovan): opt: avoid quadratic cost of string += string. + for _, edit := range edits[1:] { + between := src[prev.End:edit.Start] + if !strings.Contains(between, "\n") { + // overlapping lines: combine with previous edit. + prev.New += between + edit.New + prev.End = edit.End + } else { + // non-overlapping lines: flush previous edit. + expanded = append(expanded, expandEdit(prev, src)) + prev = edit + } + } + return append(expanded, expandEdit(prev, src)), nil // flush final edit +} + +// expandEdit returns edit expanded to complete whole lines. +func expandEdit(edit Edit, src string) Edit { + // Expand start left to start of line. + // (delta is the zero-based column number of of start.) + start := edit.Start + if delta := start - 1 - strings.LastIndex(src[:start], "\n"); delta > 0 { + edit.Start -= delta + edit.New = src[start-delta:start] + edit.New + } + + // Expand end right to end of line. + // (endCol is the zero-based column number of end.) + end := edit.End + if endCol := end - 1 - strings.LastIndex(src[:end], "\n"); endCol > 0 { + if nl := strings.IndexByte(src[end:], '\n'); nl < 0 { + edit.End = len(src) // extend to EOF + } else { + edit.End = end + nl + 1 // extend beyond \n + } + edit.New += src[end:edit.End] + } + + return edit +} diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go new file mode 100644 index 00000000000..7bb41a27bfa --- /dev/null +++ b/internal/diff/diff_test.go @@ -0,0 +1,188 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diff_test + +import ( + "math/rand" + "reflect" + "strings" + "testing" + "unicode/utf8" + + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/diff/difftest" +) + +func TestApply(t *testing.T) { + for _, tc := range difftest.TestCases { + t.Run(tc.Name, func(t *testing.T) { + got, err := diff.Apply(tc.In, tc.Edits) + if err != nil { + t.Fatalf("Apply(Edits) failed: %v", err) + } + if got != tc.Out { + t.Errorf("Apply(Edits): got %q, want %q", got, tc.Out) + } + if tc.LineEdits != nil { + got, err := diff.Apply(tc.In, tc.LineEdits) + if err != nil { + t.Fatalf("Apply(LineEdits) failed: %v", err) + } + if got != tc.Out { + t.Errorf("Apply(LineEdits): got %q, want %q", got, tc.Out) + } + } + }) + } +} + +func TestNEdits(t *testing.T) { + for _, tc := range difftest.TestCases { + edits := diff.Strings(tc.In, tc.Out) + got, err := diff.Apply(tc.In, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } + if got != tc.Out { + t.Fatalf("%s: got %q wanted %q", tc.Name, got, tc.Out) + } + if len(edits) < len(tc.Edits) { // should find subline edits + t.Errorf("got %v, expected %v for %#v", edits, tc.Edits, tc) + } + } +} + +func TestNRandom(t *testing.T) { + rand.Seed(1) + for i := 0; i < 1000; i++ { + a := randstr("abω", 16) + b := randstr("abωc", 16) + edits := diff.Strings(a, b) + got, err := diff.Apply(a, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } + if got != b { + t.Fatalf("%d: got %q, wanted %q, starting with %q", i, got, b, a) + } + } +} + +// $ go test -fuzz=FuzzRoundTrip ./internal/diff +func FuzzRoundTrip(f *testing.F) { + f.Fuzz(func(t *testing.T, a, b string) { + if !utf8.ValidString(a) || !utf8.ValidString(b) { + return // inputs must be text + } + edits := diff.Strings(a, b) + got, err := diff.Apply(a, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } + if got != b { + t.Fatalf("applying diff(%q, %q) gives %q; edits=%v", a, b, got, edits) + } + }) +} + +func TestLineEdits(t *testing.T) { + for _, tc := range difftest.TestCases { + t.Run(tc.Name, func(t *testing.T) { + // if line edits not specified, it is the same as edits + edits := tc.LineEdits + if edits == nil { + edits = tc.Edits + } + got, err := diff.LineEdits(tc.In, tc.Edits) + if err != nil { + t.Fatalf("LineEdits: %v", err) + } + if !reflect.DeepEqual(got, edits) { + t.Errorf("LineEdits got %q, want %q", got, edits) + } + }) + } +} + +func TestToUnified(t *testing.T) { + for _, tc := range difftest.TestCases { + t.Run(tc.Name, func(t *testing.T) { + unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits) + if err != nil { + t.Fatal(err) + } + if unified != tc.Unified { + t.Errorf("Unified(Edits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) + } + if tc.LineEdits != nil { + unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.LineEdits) + if err != nil { + t.Fatal(err) + } + if unified != tc.Unified { + t.Errorf("Unified(LineEdits): got diff:\n%v\nexpected:\n%v", unified, tc.Unified) + } + } + }) + } +} + +func TestRegressionOld001(t *testing.T) { + a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" + + b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" + diffs := diff.Strings(a, b) + got, err := diff.Apply(a, diffs) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } + if got != b { + i := 0 + for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ { + } + t.Errorf("oops %vd\n%q\n%q", diffs, got, b) + t.Errorf("\n%q\n%q", got[i:], b[i:]) + } +} + +func TestRegressionOld002(t *testing.T) { + a := "n\"\n)\n" + b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n" + diffs := diff.Strings(a, b) + got, err := diff.Apply(a, diffs) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } + if got != b { + i := 0 + for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ { + } + t.Errorf("oops %vd\n%q\n%q", diffs, got, b) + t.Errorf("\n%q\n%q", got[i:], b[i:]) + } +} + +// return a random string of length n made of characters from s +func randstr(s string, n int) string { + src := []rune(s) + x := make([]rune, n) + for i := 0; i < n; i++ { + x[i] = src[rand.Intn(len(src))] + } + return string(x) +} + +// return some random lines, all ending with \n +func randlines(s string, n int) string { + src := []rune(s) + var b strings.Builder + for i := 0; i < n; i++ { + for j := 0; j < 4+rand.Intn(4); j++ { + b.WriteRune(src[rand.Intn(len(src))]) + } + b.WriteByte('\n') + } + return b.String() +} diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go new file mode 100644 index 00000000000..cd026bee490 --- /dev/null +++ b/internal/diff/difftest/difftest.go @@ -0,0 +1,264 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package difftest supplies a set of tests that will operate on any +// implementation of a diff algorithm as exposed by +// "golang.org/x/tools/internal/diff" +package difftest + +import ( + "testing" + + "golang.org/x/tools/internal/diff" +) + +const ( + FileA = "from" + FileB = "to" + UnifiedPrefix = "--- " + FileA + "\n+++ " + FileB + "\n" +) + +var TestCases = []struct { + Name, In, Out, Unified string + Edits, LineEdits []diff.Edit + NoDiff bool +}{{ + Name: "empty", + In: "", + Out: "", +}, { + Name: "no_diff", + In: "gargantuan\n", + Out: "gargantuan\n", +}, { + Name: "replace_all", + In: "fruit\n", + Out: "cheese\n", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-fruit ++cheese +`[1:], + Edits: []diff.Edit{{Start: 0, End: 5, New: "cheese"}}, + LineEdits: []diff.Edit{{Start: 0, End: 6, New: "cheese\n"}}, +}, { + Name: "insert_rune", + In: "gord\n", + Out: "gourd\n", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-gord ++gourd +`[1:], + Edits: []diff.Edit{{Start: 2, End: 2, New: "u"}}, + LineEdits: []diff.Edit{{Start: 0, End: 5, New: "gourd\n"}}, +}, { + Name: "delete_rune", + In: "groat\n", + Out: "goat\n", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-groat ++goat +`[1:], + Edits: []diff.Edit{{Start: 1, End: 2, New: ""}}, + LineEdits: []diff.Edit{{Start: 0, End: 6, New: "goat\n"}}, +}, { + Name: "replace_rune", + In: "loud\n", + Out: "lord\n", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-loud ++lord +`[1:], + Edits: []diff.Edit{{Start: 2, End: 3, New: "r"}}, + LineEdits: []diff.Edit{{Start: 0, End: 5, New: "lord\n"}}, +}, { + Name: "replace_partials", + In: "blanket\n", + Out: "bunker\n", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-blanket ++bunker +`[1:], + Edits: []diff.Edit{ + {Start: 1, End: 3, New: "u"}, + {Start: 6, End: 7, New: "r"}, + }, + LineEdits: []diff.Edit{{Start: 0, End: 8, New: "bunker\n"}}, +}, { + Name: "insert_line", + In: "1: one\n3: three\n", + Out: "1: one\n2: two\n3: three\n", + Unified: UnifiedPrefix + ` +@@ -1,2 +1,3 @@ + 1: one ++2: two + 3: three +`[1:], + Edits: []diff.Edit{{Start: 7, End: 7, New: "2: two\n"}}, +}, { + Name: "replace_no_newline", + In: "A", + Out: "B", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-A +\ No newline at end of file ++B +\ No newline at end of file +`[1:], + Edits: []diff.Edit{{Start: 0, End: 1, New: "B"}}, +}, { + Name: "append_empty", + In: "", // GNU diff -u special case: -0,0 + Out: "AB\nC", + Unified: UnifiedPrefix + ` +@@ -0,0 +1,2 @@ ++AB ++C +\ No newline at end of file +`[1:], + Edits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}}, + LineEdits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}}, +}, + // TODO(adonovan): fix this test: GNU diff -u prints "+1,2", Unifies prints "+1,3". + // { + // Name: "add_start", + // In: "A", + // Out: "B\nCA", + // Unified: UnifiedPrefix + ` + // @@ -1 +1,2 @@ + // -A + // \ No newline at end of file + // +B + // +CA + // \ No newline at end of file + // `[1:], + // Edits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "B\nC"}}, + // LineEdits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "B\nC"}}, + // }, + { + Name: "add_end", + In: "A", + Out: "AB", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-A +\ No newline at end of file ++AB +\ No newline at end of file +`[1:], + Edits: []diff.Edit{{Start: 1, End: 1, New: "B"}}, + LineEdits: []diff.Edit{{Start: 0, End: 1, New: "AB"}}, + }, { + Name: "add_empty", + In: "", + Out: "AB\nC", + Unified: UnifiedPrefix + ` +@@ -0,0 +1,2 @@ ++AB ++C +\ No newline at end of file +`[1:], + Edits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}}, + LineEdits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}}, + }, { + Name: "add_newline", + In: "A", + Out: "A\n", + Unified: UnifiedPrefix + ` +@@ -1 +1 @@ +-A +\ No newline at end of file ++A +`[1:], + Edits: []diff.Edit{{Start: 1, End: 1, New: "\n"}}, + LineEdits: []diff.Edit{{Start: 0, End: 1, New: "A\n"}}, + }, { + Name: "delete_front", + In: "A\nB\nC\nA\nB\nB\nA\n", + Out: "C\nB\nA\nB\nA\nC\n", + Unified: UnifiedPrefix + ` +@@ -1,7 +1,6 @@ +-A +-B + C ++B + A + B +-B + A ++C +`[1:], + NoDiff: true, // unified diff is different but valid + Edits: []diff.Edit{ + {Start: 0, End: 4, New: ""}, + {Start: 6, End: 6, New: "B\n"}, + {Start: 10, End: 12, New: ""}, + {Start: 14, End: 14, New: "C\n"}, + }, + }, { + Name: "replace_last_line", + In: "A\nB\n", + Out: "A\nC\n\n", + Unified: UnifiedPrefix + ` +@@ -1,2 +1,3 @@ + A +-B ++C ++ +`[1:], + Edits: []diff.Edit{{Start: 2, End: 3, New: "C\n"}}, + LineEdits: []diff.Edit{{Start: 2, End: 4, New: "C\n\n"}}, + }, + { + Name: "multiple_replace", + In: "A\nB\nC\nD\nE\nF\nG\n", + Out: "A\nH\nI\nJ\nE\nF\nK\n", + Unified: UnifiedPrefix + ` +@@ -1,7 +1,7 @@ + A +-B +-C +-D ++H ++I ++J + E + F +-G ++K +`[1:], + Edits: []diff.Edit{ + {Start: 2, End: 8, New: "H\nI\nJ\n"}, + {Start: 12, End: 14, New: "K\n"}, + }, + NoDiff: true, // diff algorithm produces different delete/insert pattern + }, +} + +func DiffTest(t *testing.T, compute func(before, after string) []diff.Edit) { + for _, test := range TestCases { + t.Run(test.Name, func(t *testing.T) { + edits := compute(test.In, test.Out) + got, err := diff.Apply(test.In, edits) + if err != nil { + t.Fatalf("Apply failed: %v", err) + } + unified, err := diff.ToUnified(FileA, FileB, test.In, edits) + if err != nil { + t.Fatalf("ToUnified: %v", err) + } + if got != test.Out { + t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) + } + if !test.NoDiff && unified != test.Unified { + t.Errorf("Unified: got diff:\n%v\nexpected:\n%v", unified, test.Unified) + } + }) + } +} diff --git a/internal/lsp/diff/difftest/difftest_test.go b/internal/diff/difftest/difftest_test.go similarity index 92% rename from internal/lsp/diff/difftest/difftest_test.go rename to internal/diff/difftest/difftest_test.go index fd7ecf95997..a990e522438 100644 --- a/internal/lsp/diff/difftest/difftest_test.go +++ b/internal/diff/difftest/difftest_test.go @@ -4,7 +4,7 @@ // Package difftest supplies a set of tests that will operate on any // implementation of a diff algorithm as exposed by -// "golang.org/x/tools/internal/lsp/diff" +// "golang.org/x/tools/internal/diff" package difftest_test import ( @@ -15,7 +15,7 @@ import ( "strings" "testing" - "golang.org/x/tools/internal/lsp/diff/difftest" + "golang.org/x/tools/internal/diff/difftest" "golang.org/x/tools/internal/testenv" ) @@ -23,7 +23,6 @@ func TestVerifyUnified(t *testing.T) { testenv.NeedsTool(t, "diff") for _, test := range difftest.TestCases { t.Run(test.Name, func(t *testing.T) { - t.Helper() if test.NoDiff { t.Skip("diff tool produces expected different results") } @@ -35,7 +34,7 @@ func TestVerifyUnified(t *testing.T) { diff = difftest.UnifiedPrefix + diff } if diff != test.Unified { - t.Errorf("unified:\n%q\ndiff -u:\n%q", test.Unified, diff) + t.Errorf("unified:\n%s\ndiff -u:\n%s", test.Unified, diff) } }) } diff --git a/internal/diff/export_test.go b/internal/diff/export_test.go new file mode 100644 index 00000000000..eedf0dd77ba --- /dev/null +++ b/internal/diff/export_test.go @@ -0,0 +1,9 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diff + +// This file exports some private declarations to tests. + +var LineEdits = lineEdits diff --git a/internal/diff/lcs/common.go b/internal/diff/lcs/common.go new file mode 100644 index 00000000000..e5d08014760 --- /dev/null +++ b/internal/diff/lcs/common.go @@ -0,0 +1,184 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "log" + "sort" +) + +// lcs is a longest common sequence +type lcs []diag + +// A diag is a piece of the edit graph where A[X+i] == B[Y+i], for 0<=i l[j].Len + }) + return l +} + +// validate that the elements of the lcs do not overlap +// (can only happen when the two-sided algorithm ends early) +// expects the lcs to be sorted +func (l lcs) valid() bool { + for i := 1; i < len(l); i++ { + if l[i-1].X+l[i-1].Len > l[i].X { + return false + } + if l[i-1].Y+l[i-1].Len > l[i].Y { + return false + } + } + return true +} + +// repair overlapping lcs +// only called if two-sided stops early +func (l lcs) fix() lcs { + // from the set of diagonals in l, find a maximal non-conflicting set + // this problem may be NP-complete, but we use a greedy heuristic, + // which is quadratic, but with a better data structure, could be D log D. + // indepedent is not enough: {0,3,1} and {3,0,2} can't both occur in an lcs + // which has to have monotone x and y + if len(l) == 0 { + return nil + } + sort.Slice(l, func(i, j int) bool { return l[i].Len > l[j].Len }) + tmp := make(lcs, 0, len(l)) + tmp = append(tmp, l[0]) + for i := 1; i < len(l); i++ { + var dir direction + nxt := l[i] + for _, in := range tmp { + if dir, nxt = overlap(in, nxt); dir == empty || dir == bad { + break + } + } + if nxt.Len > 0 && dir != bad { + tmp = append(tmp, nxt) + } + } + tmp.sort() + if false && !tmp.valid() { // debug checking + log.Fatalf("here %d", len(tmp)) + } + return tmp +} + +type direction int + +const ( + empty direction = iota // diag is empty (so not in lcs) + leftdown // proposed acceptably to the left and below + rightup // proposed diag is acceptably to the right and above + bad // proposed diag is inconsistent with the lcs so far +) + +// overlap trims the proposed diag prop so it doesn't overlap with +// the existing diag that has already been added to the lcs. +func overlap(exist, prop diag) (direction, diag) { + if prop.X <= exist.X && exist.X < prop.X+prop.Len { + // remove the end of prop where it overlaps with the X end of exist + delta := prop.X + prop.Len - exist.X + prop.Len -= delta + if prop.Len <= 0 { + return empty, prop + } + } + if exist.X <= prop.X && prop.X < exist.X+exist.Len { + // remove the beginning of prop where overlaps with exist + delta := exist.X + exist.Len - prop.X + prop.Len -= delta + if prop.Len <= 0 { + return empty, prop + } + prop.X += delta + prop.Y += delta + } + if prop.Y <= exist.Y && exist.Y < prop.Y+prop.Len { + // remove the end of prop that overlaps (in Y) with exist + delta := prop.Y + prop.Len - exist.Y + prop.Len -= delta + if prop.Len <= 0 { + return empty, prop + } + } + if exist.Y <= prop.Y && prop.Y < exist.Y+exist.Len { + // remove the beginning of peop that overlaps with exist + delta := exist.Y + exist.Len - prop.Y + prop.Len -= delta + if prop.Len <= 0 { + return empty, prop + } + prop.X += delta // no test reaches this code + prop.Y += delta + } + if prop.X+prop.Len <= exist.X && prop.Y+prop.Len <= exist.Y { + return leftdown, prop + } + if exist.X+exist.Len <= prop.X && exist.Y+exist.Len <= prop.Y { + return rightup, prop + } + // prop can't be in an lcs that contains exist + return bad, prop +} + +// manipulating Diag and lcs + +// prependlcs a diagonal (x,y)-(x+1,y+1) segment either to an empty lcs +// or to its first Diag. prependlcs is only called extending diagonals +// the backward direction. +func prependlcs(lcs lcs, x, y int) lcs { + if len(lcs) > 0 { + d := &lcs[0] + if int(d.X) == x+1 && int(d.Y) == y+1 { + // extend the diagonal down and to the left + d.X, d.Y = int(x), int(y) + d.Len++ + return lcs + } + } + + r := diag{X: int(x), Y: int(y), Len: 1} + lcs = append([]diag{r}, lcs...) + return lcs +} + +// appendlcs appends a diagonal, or extends the existing one. +// by adding the edge (x,y)-(x+1.y+1). appendlcs is only called +// while extending diagonals in the forward direction. +func appendlcs(lcs lcs, x, y int) lcs { + if len(lcs) > 0 { + last := &lcs[len(lcs)-1] + // Expand last element if adjoining. + if last.X+last.Len == x && last.Y+last.Len == y { + last.Len++ + return lcs + } + } + + return append(lcs, diag{X: x, Y: y, Len: 1}) +} + +// enforce constraint on d, k +func ok(d, k int) bool { + return d >= 0 && -d <= k && k <= d +} + +type Diff struct { + Start, End int // offsets in A + Text string // replacement text +} diff --git a/internal/diff/lcs/common_test.go b/internal/diff/lcs/common_test.go new file mode 100644 index 00000000000..4aa36abc2e8 --- /dev/null +++ b/internal/diff/lcs/common_test.go @@ -0,0 +1,140 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "log" + "math/rand" + "strings" + "testing" +) + +type Btest struct { + a, b string + lcs []string +} + +var Btests = []Btest{ + {"aaabab", "abaab", []string{"abab", "aaab"}}, + {"aabbba", "baaba", []string{"aaba"}}, + {"cabbx", "cbabx", []string{"cabx", "cbbx"}}, + {"c", "cb", []string{"c"}}, + {"aaba", "bbb", []string{"b"}}, + {"bbaabb", "b", []string{"b"}}, + {"baaabb", "bbaba", []string{"bbb", "baa", "bab"}}, + {"baaabb", "abbab", []string{"abb", "bab", "aab"}}, + {"baaba", "aaabba", []string{"aaba"}}, + {"ca", "cba", []string{"ca"}}, + {"ccbcbc", "abba", []string{"bb"}}, + {"ccbcbc", "aabba", []string{"bb"}}, + {"ccb", "cba", []string{"cb"}}, + {"caef", "axe", []string{"ae"}}, + {"bbaabb", "baabb", []string{"baabb"}}, + // Example from Myers: + {"abcabba", "cbabac", []string{"caba", "baba", "cbba"}}, + {"3456aaa", "aaa", []string{"aaa"}}, + {"aaa", "aaa123", []string{"aaa"}}, + {"aabaa", "aacaa", []string{"aaaa"}}, + {"1a", "a", []string{"a"}}, + {"abab", "bb", []string{"bb"}}, + {"123", "ab", []string{""}}, + {"a", "b", []string{""}}, + {"abc", "123", []string{""}}, + {"aa", "aa", []string{"aa"}}, + {"abcde", "12345", []string{""}}, + {"aaa3456", "aaa", []string{"aaa"}}, + {"abcde", "12345a", []string{"a"}}, + {"ab", "123", []string{""}}, + {"1a2", "a", []string{"a"}}, + // for two-sided + {"babaab", "cccaba", []string{"aba"}}, + {"aabbab", "cbcabc", []string{"bab"}}, + {"abaabb", "bcacab", []string{"baab"}}, + {"abaabb", "abaaaa", []string{"abaa"}}, + {"bababb", "baaabb", []string{"baabb"}}, + {"abbbaa", "cabacc", []string{"aba"}}, + {"aabbaa", "aacaba", []string{"aaaa", "aaba"}}, +} + +func init() { + log.SetFlags(log.Lshortfile) +} + +func check(t *testing.T, str string, lcs lcs, want []string) { + t.Helper() + if !lcs.valid() { + t.Errorf("bad lcs %v", lcs) + } + var got strings.Builder + for _, dd := range lcs { + got.WriteString(str[dd.X : dd.X+dd.Len]) + } + ans := got.String() + for _, w := range want { + if ans == w { + return + } + } + t.Fatalf("str=%q lcs=%v want=%q got=%q", str, lcs, want, ans) +} + +func checkDiffs(t *testing.T, before string, diffs []Diff, after string) { + t.Helper() + var ans strings.Builder + sofar := 0 // index of position in before + for _, d := range diffs { + if sofar < d.Start { + ans.WriteString(before[sofar:d.Start]) + } + ans.WriteString(d.Text) + sofar = d.End + } + ans.WriteString(before[sofar:]) + if ans.String() != after { + t.Fatalf("diff %v took %q to %q, not to %q", diffs, before, ans.String(), after) + } +} + +func lcslen(l lcs) int { + ans := 0 + for _, d := range l { + ans += int(d.Len) + } + return ans +} + +// return a random string of length n made of characters from s +func randstr(s string, n int) string { + src := []rune(s) + x := make([]rune, n) + for i := 0; i < n; i++ { + x[i] = src[rand.Intn(len(src))] + } + return string(x) +} + +func TestLcsFix(t *testing.T) { + tests := []struct{ before, after lcs }{ + {lcs{diag{0, 0, 3}, diag{2, 2, 5}, diag{3, 4, 5}, diag{8, 9, 4}}, lcs{diag{0, 0, 2}, diag{2, 2, 1}, diag{3, 4, 5}, diag{8, 9, 4}}}, + {lcs{diag{1, 1, 6}, diag{6, 12, 3}}, lcs{diag{1, 1, 5}, diag{6, 12, 3}}}, + {lcs{diag{0, 0, 4}, diag{3, 5, 4}}, lcs{diag{0, 0, 3}, diag{3, 5, 4}}}, + {lcs{diag{0, 20, 1}, diag{0, 0, 3}, diag{1, 20, 4}}, lcs{diag{0, 0, 3}, diag{3, 22, 2}}}, + {lcs{diag{0, 0, 4}, diag{1, 1, 2}}, lcs{diag{0, 0, 4}}}, + {lcs{diag{0, 0, 4}}, lcs{diag{0, 0, 4}}}, + {lcs{}, lcs{}}, + {lcs{diag{0, 0, 4}, diag{1, 1, 6}, diag{3, 3, 2}}, lcs{diag{0, 0, 1}, diag{1, 1, 6}}}, + } + for n, x := range tests { + got := x.before.fix() + if len(got) != len(x.after) { + t.Errorf("got %v, expected %v, for %v", got, x.after, x.before) + } + olen := lcslen(x.after) + glen := lcslen(got) + if olen != glen { + t.Errorf("%d: lens(%d,%d) differ, %v, %v, %v", n, glen, olen, got, x.after, x.before) + } + } +} diff --git a/internal/diff/lcs/doc.go b/internal/diff/lcs/doc.go new file mode 100644 index 00000000000..dc779f38a01 --- /dev/null +++ b/internal/diff/lcs/doc.go @@ -0,0 +1,156 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// package lcs contains code to find longest-common-subsequences +// (and diffs) +package lcs + +/* +Compute longest-common-subsequences of two slices A, B using +algorithms from Myers' paper. A longest-common-subsequence +(LCS from now on) of A and B is a maximal set of lexically increasing +pairs of subscripts (x,y) with A[x]==B[y]. There may be many LCS, but +they all have the same length. An LCS determines a sequence of edits +that changes A into B. + +The key concept is the edit graph of A and B. +If A has length N and B has length M, then the edit graph has +vertices v[i][j] for 0 <= i <= N, 0 <= j <= M. There is a +horizontal edge from v[i][j] to v[i+1][j] whenever both are in +the graph, and a vertical edge from v[i][j] to f[i][j+1] similarly. +When A[i] == B[j] there is a diagonal edge from v[i][j] to v[i+1][j+1]. + +A path between in the graph between (0,0) and (N,M) determines a sequence +of edits converting A into B: each horizontal edge corresponds to removing +an element of A, and each vertical edge corresponds to inserting an +element of B. + +A vertex (x,y) is on (forward) diagonal k if x-y=k. A path in the graph +is of length D if it has D non-diagonal edges. The algorithms generate +forward paths (in which at least one of x,y increases at each edge), +or backward paths (in which at least one of x,y decreases at each edge), +or a combination. (Note that the orientation is the traditional mathematical one, +with the origin in the lower-left corner.) + +Here is the edit graph for A:"aabbaa", B:"aacaba". (I know the diagonals look weird.) + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + b | | | ___/‾‾‾ | ___/‾‾‾ | | | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + c | | | | | | | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a a b b a a + + +The algorithm labels a vertex (x,y) with D,k if it is on diagonal k and at +the end of a maximal path of length D. (Because x-y=k it suffices to remember +only the x coordinate of the vertex.) + +The forward algorithm: Find the longest diagonal starting at (0,0) and +label its end with D=0,k=0. From that vertex take a vertical step and +then follow the longest diagonal (up and to the right), and label that vertex +with D=1,k=-1. From the D=0,k=0 point take a horizontal step and the follow +the longest diagonal (up and to the right) and label that vertex +D=1,k=1. In the same way, having labelled all the D vertices, +from a vertex labelled D,k find two vertices +tentatively labelled D+1,k-1 and D+1,k+1. There may be two on the same +diagonal, in which case take the one with the larger x. + +Eventually the path gets to (N,M), and the diagonals on it are the LCS. + +Here is the edit graph with the ends of D-paths labelled. (So, for instance, +0/2,2 indicates that x=2,y=2 is labelled with 0, as it should be, since the first +step is to go up the longest diagonal from (0,0).) +A:"aabbaa", B:"aacaba" + ⊙ ------- ⊙ ------- ⊙ -------(3/3,6)------- ⊙ -------(3/5,6)-------(4/6,6) + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ -------(2/3,5)------- ⊙ ------- ⊙ ------- ⊙ + b | | | ___/‾‾‾ | ___/‾‾‾ | | | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ -------(3/5,4)------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ -------(1/2,3)-------(2/3,3)------- ⊙ ------- ⊙ ------- ⊙ + c | | | | | | | + ⊙ ------- ⊙ -------(0/2,2)-------(1/3,2)-------(2/4,2)-------(3/5,2)-------(4/6,2) + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ | + ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ + a a b b a a + +The 4-path is reconstructed starting at (4/6,6), horizontal to (3/5,6), diagonal to (3,4), vertical +to (2/3,3), horizontal to (1/2,3), vertical to (0/2,2), and diagonal to (0,0). As expected, +there are 4 non-diagonal steps, and the diagonals form an LCS. + +There is a symmetric backward algorithm, which gives (backwards labels are prefixed with a colon): +A:"aabbaa", B:"aacaba" + ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ + a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ | + ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ --------(:0/5,5)-------- ⊙ + b | | | ____/‾‾‾ | ____/‾‾‾ | | | + ⊙ -------- ⊙ -------- ⊙ --------(:1/3,4)-------- ⊙ -------- ⊙ -------- ⊙ + a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ | + (:3/0,3)--------(:2/1,3)-------- ⊙ --------(:2/3,3)--------(:1/4,3)-------- ⊙ -------- ⊙ + c | | | | | | | + ⊙ -------- ⊙ -------- ⊙ --------(:3/3,2)--------(:2/4,2)-------- ⊙ -------- ⊙ + a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ | + (:3/0,1)-------- ⊙ -------- ⊙ -------- ⊙ --------(:3/4,1)-------- ⊙ -------- ⊙ + a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ | + (:4/0,0)-------- ⊙ -------- ⊙ -------- ⊙ --------(:4/4,0)-------- ⊙ -------- ⊙ + a a b b a a + +Neither of these is ideal for use in an editor, where it is undesirable to send very long diffs to the +front end. It's tricky to decide exactly what 'very long diffs' means, as "replace A by B" is very short. +We want to control how big D can be, by stopping when it gets too large. The forward algorithm then +privileges common prefixes, and the backward algorithm privileges common suffixes. Either is an undesirable +asymmetry. + +Fortunately there is a two-sided algorithm, implied by results in Myers' paper. Here's what the labels in +the edit graph look like. +A:"aabbaa", B:"aacaba" + ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ + a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ | + ⊙ --------- ⊙ --------- ⊙ --------- (2/3,5) --------- ⊙ --------- (:0/5,5)--------- ⊙ + b | | | ____/‾‾‾‾ | ____/‾‾‾‾ | | | + ⊙ --------- ⊙ --------- ⊙ --------- (:1/3,4)--------- ⊙ --------- ⊙ --------- ⊙ + a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ | + ⊙ --------- (:2/1,3)--------- (1/2,3) ---------(2:2/3,3)--------- (:1/4,3)--------- ⊙ --------- ⊙ + c | | | | | | | + ⊙ --------- ⊙ --------- (0/2,2) --------- (1/3,2) ---------(2:2/4,2)--------- ⊙ --------- ⊙ + a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ | + ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ + a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ | + ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ + a a b b a a + +The algorithm stopped when it saw the backwards 2-path ending at (1,3) and the forwards 2-path ending at (3,5). The criterion +is a backwards path ending at (u,v) and a forward path ending at (x,y), where u <= x and the two points are on the same +diagonal. (Here the edgegraph has a diagonal, but the criterion is x-y=u-v.) Myers proves there is a forward +2-path from (0,0) to (1,3), and that together with the backwards 2-path ending at (1,3) gives the expected 4-path. +Unfortunately the forward path has to be constructed by another run of the forward algorithm; it can't be found from the +computed labels. That is the worst case. Had the code noticed (x,y)=(u,v)=(3,3) the whole path could be reconstructed +from the edgegraph. The implementation looks for a number of special cases to try to avoid computing an extra forward path. + +If the two-sided algorithm has stop early (because D has become too large) it will have found a forward LCS and a +backwards LCS. Ideally these go with disjoint prefixes and suffixes of A and B, but disjointness may fail and the two +computed LCS may conflict. (An easy example is where A is a suffix of B, and shares a short prefix. The backwards LCS +is all of A, and the forward LCS is a prefix of A.) The algorithm combines the two +to form a best-effort LCS. In the worst case the forward partial LCS may have to +be recomputed. +*/ + +/* Eugene Myers paper is titled +"An O(ND) Difference Algorithm and Its Variations" +and can be found at +http://www.xmailserver.org/diff2.pdf + +(There is a generic implementation of the algorithm the the repository with git hash +b9ad7e4ade3a686d608e44475390ad428e60e7fc) +*/ diff --git a/internal/diff/lcs/git.sh b/internal/diff/lcs/git.sh new file mode 100644 index 00000000000..6856f843958 --- /dev/null +++ b/internal/diff/lcs/git.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Copyright 2022 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +# +# Creates a zip file containing all numbered versions +# of the commit history of a large source file, for use +# as input data for the tests of the diff algorithm. +# +# Run script from root of the x/tools repo. + +set -eu + +# WARNING: This script will install the latest version of $file +# The largest real source file in the x/tools repo. +# file=internal/lsp/source/completion/completion.go +# file=internal/lsp/source/diagnostics.go +file=internal/lsp/protocol/tsprotocol.go + +tmp=$(mktemp -d) +git log $file | + awk '/^commit / {print $2}' | + nl -ba -nrz | + while read n hash; do + git checkout --quiet $hash $file + cp -f $file $tmp/$n + done +(cd $tmp && zip -q - *) > testdata.zip +rm -fr $tmp +git restore --staged $file +git restore $file +echo "Created testdata.zip" diff --git a/internal/diff/lcs/labels.go b/internal/diff/lcs/labels.go new file mode 100644 index 00000000000..0689f1ed700 --- /dev/null +++ b/internal/diff/lcs/labels.go @@ -0,0 +1,55 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "fmt" +) + +// For each D, vec[D] has length D+1, +// and the label for (D, k) is stored in vec[D][(D+k)/2]. +type label struct { + vec [][]int +} + +// Temporary checking DO NOT COMMIT true TO PRODUCTION CODE +const debug = false + +// debugging. check that the (d,k) pair is valid +// (that is, -d<=k<=d and d+k even) +func checkDK(D, k int) { + if k >= -D && k <= D && (D+k)%2 == 0 { + return + } + panic(fmt.Sprintf("out of range, d=%d,k=%d", D, k)) +} + +func (t *label) set(D, k, x int) { + if debug { + checkDK(D, k) + } + for len(t.vec) <= D { + t.vec = append(t.vec, nil) + } + if t.vec[D] == nil { + t.vec[D] = make([]int, D+1) + } + t.vec[D][(D+k)/2] = x // known that D+k is even +} + +func (t *label) get(d, k int) int { + if debug { + checkDK(d, k) + } + return int(t.vec[d][(d+k)/2]) +} + +func newtriang(limit int) label { + if limit < 100 { + // Preallocate if limit is not large. + return label{vec: make([][]int, limit)} + } + return label{} +} diff --git a/internal/diff/lcs/old.go b/internal/diff/lcs/old.go new file mode 100644 index 00000000000..a091edd5501 --- /dev/null +++ b/internal/diff/lcs/old.go @@ -0,0 +1,530 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "fmt" + "strings" +) + +// non generic code. The names have Old at the end to indicate they are the +// the implementation that doesn't use generics. + +// Compute the Diffs and the lcs. +func Compute(a, b interface{}, limit int) ([]Diff, lcs) { + var ans lcs + g := newegraph(a, b, limit) + ans = g.twosided() + diffs := g.fromlcs(ans) + return diffs, ans +} + +// editGraph carries the information for computing the lcs for []byte, []rune, or []string. +type editGraph struct { + eq eq // how to compare elements of A, B, and convert slices to strings + vf, vb label // forward and backward labels + + limit int // maximal value of D + // the bounding rectangle of the current edit graph + lx, ly, ux, uy int + delta int // common subexpression: (ux-lx)-(uy-ly) +} + +// abstraction in place of generic +type eq interface { + eq(i, j int) bool + substr(i, j int) string // string from b[i:j] + lena() int + lenb() int +} + +type byteeq struct { + a, b []byte // the input was ascii. perhaps these could be strings +} + +func (x *byteeq) eq(i, j int) bool { return x.a[i] == x.b[j] } +func (x *byteeq) substr(i, j int) string { return string(x.b[i:j]) } +func (x *byteeq) lena() int { return int(len(x.a)) } +func (x *byteeq) lenb() int { return int(len(x.b)) } + +type runeeq struct { + a, b []rune +} + +func (x *runeeq) eq(i, j int) bool { return x.a[i] == x.b[j] } +func (x *runeeq) substr(i, j int) string { return string(x.b[i:j]) } +func (x *runeeq) lena() int { return int(len(x.a)) } +func (x *runeeq) lenb() int { return int(len(x.b)) } + +type lineeq struct { + a, b []string +} + +func (x *lineeq) eq(i, j int) bool { return x.a[i] == x.b[j] } +func (x *lineeq) substr(i, j int) string { return strings.Join(x.b[i:j], "") } +func (x *lineeq) lena() int { return int(len(x.a)) } +func (x *lineeq) lenb() int { return int(len(x.b)) } + +func neweq(a, b interface{}) eq { + switch x := a.(type) { + case []byte: + return &byteeq{a: x, b: b.([]byte)} + case []rune: + return &runeeq{a: x, b: b.([]rune)} + case []string: + return &lineeq{a: x, b: b.([]string)} + default: + panic(fmt.Sprintf("unexpected type %T in neweq", x)) + } +} + +func (g *editGraph) fromlcs(lcs lcs) []Diff { + var ans []Diff + var pa, pb int // offsets in a, b + for _, l := range lcs { + if pa < l.X && pb < l.Y { + ans = append(ans, Diff{pa, l.X, g.eq.substr(pb, l.Y)}) + } else if pa < l.X { + ans = append(ans, Diff{pa, l.X, ""}) + } else if pb < l.Y { + ans = append(ans, Diff{pa, l.X, g.eq.substr(pb, l.Y)}) + } + pa = l.X + l.Len + pb = l.Y + l.Len + } + if pa < g.eq.lena() && pb < g.eq.lenb() { + ans = append(ans, Diff{pa, g.eq.lena(), g.eq.substr(pb, g.eq.lenb())}) + } else if pa < g.eq.lena() { + ans = append(ans, Diff{pa, g.eq.lena(), ""}) + } else if pb < g.eq.lenb() { + ans = append(ans, Diff{pa, g.eq.lena(), g.eq.substr(pb, g.eq.lenb())}) + } + return ans +} + +func newegraph(a, b interface{}, limit int) *editGraph { + if limit <= 0 { + limit = 1 << 25 // effectively infinity + } + var alen, blen int + switch a := a.(type) { + case []byte: + alen, blen = len(a), len(b.([]byte)) + case []rune: + alen, blen = len(a), len(b.([]rune)) + case []string: + alen, blen = len(a), len(b.([]string)) + default: + panic(fmt.Sprintf("unexpected type %T in newegraph", a)) + } + ans := &editGraph{eq: neweq(a, b), vf: newtriang(limit), vb: newtriang(limit), limit: int(limit), + ux: alen, uy: blen, delta: alen - blen} + return ans +} + +// --- FORWARD --- +// fdone decides if the forwward path has reached the upper right +// corner of the rectangele. If so, it also returns the computed lcs. +func (e *editGraph) fdone(D, k int) (bool, lcs) { + // x, y, k are relative to the rectangle + x := e.vf.get(D, k) + y := x - k + if x == e.ux && y == e.uy { + return true, e.forwardlcs(D, k) + } + return false, nil +} + +// run the forward algorithm, until success or up to the limit on D. +func (e *editGraph) forward() lcs { + e.setForward(0, 0, e.lx) + if ok, ans := e.fdone(0, 0); ok { + return ans + } + // from D to D+1 + for D := 0; D < e.limit; D++ { + e.setForward(D+1, -(D + 1), e.getForward(D, -D)) + if ok, ans := e.fdone(D+1, -(D + 1)); ok { + return ans + } + e.setForward(D+1, D+1, e.getForward(D, D)+1) + if ok, ans := e.fdone(D+1, D+1); ok { + return ans + } + for k := -D + 1; k <= D-1; k += 2 { + // these are tricky and easy to get backwards + lookv := e.lookForward(k, e.getForward(D, k-1)+1) + lookh := e.lookForward(k, e.getForward(D, k+1)) + if lookv > lookh { + e.setForward(D+1, k, lookv) + } else { + e.setForward(D+1, k, lookh) + } + if ok, ans := e.fdone(D+1, k); ok { + return ans + } + } + } + // D is too large + // find the D path with maximal x+y inside the rectangle and + // use that to compute the found part of the lcs + kmax := -e.limit - 1 + diagmax := -1 + for k := -e.limit; k <= e.limit; k += 2 { + x := e.getForward(e.limit, k) + y := x - k + if x+y > diagmax && x <= e.ux && y <= e.uy { + diagmax, kmax = x+y, k + } + } + return e.forwardlcs(e.limit, kmax) +} + +// recover the lcs by backtracking from the farthest point reached +func (e *editGraph) forwardlcs(D, k int) lcs { + var ans lcs + for x := e.getForward(D, k); x != 0 || x-k != 0; { + if ok(D-1, k-1) && x-1 == e.getForward(D-1, k-1) { + // if (x-1,y) is labelled D-1, x--,D--,k--,continue + D, k, x = D-1, k-1, x-1 + continue + } else if ok(D-1, k+1) && x == e.getForward(D-1, k+1) { + // if (x,y-1) is labelled D-1, x, D--,k++, continue + D, k = D-1, k+1 + continue + } + // if (x-1,y-1)--(x,y) is a diagonal, prepend,x--,y--, continue + y := x - k + realx, realy := x+e.lx, y+e.ly + if e.eq.eq(realx-1, realy-1) { + ans = prependlcs(ans, realx-1, realy-1) + x-- + } else { + panic("broken path") + } + } + return ans +} + +// start at (x,y), go up the diagonal as far as possible, +// and label the result with d +func (e *editGraph) lookForward(k, relx int) int { + rely := relx - k + x, y := relx+e.lx, rely+e.ly + for x < e.ux && y < e.uy && e.eq.eq(x, y) { + x++ + y++ + } + return x +} + +func (e *editGraph) setForward(d, k, relx int) { + x := e.lookForward(k, relx) + e.vf.set(d, k, x-e.lx) +} + +func (e *editGraph) getForward(d, k int) int { + x := e.vf.get(d, k) + return x +} + +// --- BACKWARD --- +// bdone decides if the backward path has reached the lower left corner +func (e *editGraph) bdone(D, k int) (bool, lcs) { + // x, y, k are relative to the rectangle + x := e.vb.get(D, k) + y := x - (k + e.delta) + if x == 0 && y == 0 { + return true, e.backwardlcs(D, k) + } + return false, nil +} + +// run the backward algorithm, until success or up to the limit on D. +func (e *editGraph) backward() lcs { + e.setBackward(0, 0, e.ux) + if ok, ans := e.bdone(0, 0); ok { + return ans + } + // from D to D+1 + for D := 0; D < e.limit; D++ { + e.setBackward(D+1, -(D + 1), e.getBackward(D, -D)-1) + if ok, ans := e.bdone(D+1, -(D + 1)); ok { + return ans + } + e.setBackward(D+1, D+1, e.getBackward(D, D)) + if ok, ans := e.bdone(D+1, D+1); ok { + return ans + } + for k := -D + 1; k <= D-1; k += 2 { + // these are tricky and easy to get wrong + lookv := e.lookBackward(k, e.getBackward(D, k-1)) + lookh := e.lookBackward(k, e.getBackward(D, k+1)-1) + if lookv < lookh { + e.setBackward(D+1, k, lookv) + } else { + e.setBackward(D+1, k, lookh) + } + if ok, ans := e.bdone(D+1, k); ok { + return ans + } + } + } + + // D is too large + // find the D path with minimal x+y inside the rectangle and + // use that to compute the part of the lcs found + kmax := -e.limit - 1 + diagmin := 1 << 25 + for k := -e.limit; k <= e.limit; k += 2 { + x := e.getBackward(e.limit, k) + y := x - (k + e.delta) + if x+y < diagmin && x >= 0 && y >= 0 { + diagmin, kmax = x+y, k + } + } + if kmax < -e.limit { + panic(fmt.Sprintf("no paths when limit=%d?", e.limit)) + } + return e.backwardlcs(e.limit, kmax) +} + +// recover the lcs by backtracking +func (e *editGraph) backwardlcs(D, k int) lcs { + var ans lcs + for x := e.getBackward(D, k); x != e.ux || x-(k+e.delta) != e.uy; { + if ok(D-1, k-1) && x == e.getBackward(D-1, k-1) { + // D--, k--, x unchanged + D, k = D-1, k-1 + continue + } else if ok(D-1, k+1) && x+1 == e.getBackward(D-1, k+1) { + // D--, k++, x++ + D, k, x = D-1, k+1, x+1 + continue + } + y := x - (k + e.delta) + realx, realy := x+e.lx, y+e.ly + if e.eq.eq(realx, realy) { + ans = appendlcs(ans, realx, realy) + x++ + } else { + panic("broken path") + } + } + return ans +} + +// start at (x,y), go down the diagonal as far as possible, +func (e *editGraph) lookBackward(k, relx int) int { + rely := relx - (k + e.delta) // forward k = k + e.delta + x, y := relx+e.lx, rely+e.ly + for x > 0 && y > 0 && e.eq.eq(x-1, y-1) { + x-- + y-- + } + return x +} + +// convert to rectangle, and label the result with d +func (e *editGraph) setBackward(d, k, relx int) { + x := e.lookBackward(k, relx) + e.vb.set(d, k, x-e.lx) +} + +func (e *editGraph) getBackward(d, k int) int { + x := e.vb.get(d, k) + return x +} + +// -- TWOSIDED --- + +func (e *editGraph) twosided() lcs { + // The termination condition could be improved, as either the forward + // or backward pass could succeed before Myers' Lemma applies. + // Aside from questions of efficiency (is the extra testing cost-effective) + // this is more likely to matter when e.limit is reached. + e.setForward(0, 0, e.lx) + e.setBackward(0, 0, e.ux) + + // from D to D+1 + for D := 0; D < e.limit; D++ { + // just finished a backwards pass, so check + if got, ok := e.twoDone(D, D); ok { + return e.twolcs(D, D, got) + } + // do a forwards pass (D to D+1) + e.setForward(D+1, -(D + 1), e.getForward(D, -D)) + e.setForward(D+1, D+1, e.getForward(D, D)+1) + for k := -D + 1; k <= D-1; k += 2 { + // these are tricky and easy to get backwards + lookv := e.lookForward(k, e.getForward(D, k-1)+1) + lookh := e.lookForward(k, e.getForward(D, k+1)) + if lookv > lookh { + e.setForward(D+1, k, lookv) + } else { + e.setForward(D+1, k, lookh) + } + } + // just did a forward pass, so check + if got, ok := e.twoDone(D+1, D); ok { + return e.twolcs(D+1, D, got) + } + // do a backward pass, D to D+1 + e.setBackward(D+1, -(D + 1), e.getBackward(D, -D)-1) + e.setBackward(D+1, D+1, e.getBackward(D, D)) + for k := -D + 1; k <= D-1; k += 2 { + // these are tricky and easy to get wrong + lookv := e.lookBackward(k, e.getBackward(D, k-1)) + lookh := e.lookBackward(k, e.getBackward(D, k+1)-1) + if lookv < lookh { + e.setBackward(D+1, k, lookv) + } else { + e.setBackward(D+1, k, lookh) + } + } + } + + // D too large. combine a forward and backward partial lcs + // first, a forward one + kmax := -e.limit - 1 + diagmax := -1 + for k := -e.limit; k <= e.limit; k += 2 { + x := e.getForward(e.limit, k) + y := x - k + if x+y > diagmax && x <= e.ux && y <= e.uy { + diagmax, kmax = x+y, k + } + } + if kmax < -e.limit { + panic(fmt.Sprintf("no forward paths when limit=%d?", e.limit)) + } + lcs := e.forwardlcs(e.limit, kmax) + // now a backward one + // find the D path with minimal x+y inside the rectangle and + // use that to compute the lcs + diagmin := 1 << 25 // infinity + for k := -e.limit; k <= e.limit; k += 2 { + x := e.getBackward(e.limit, k) + y := x - (k + e.delta) + if x+y < diagmin && x >= 0 && y >= 0 { + diagmin, kmax = x+y, k + } + } + if kmax < -e.limit { + panic(fmt.Sprintf("no backward paths when limit=%d?", e.limit)) + } + lcs = append(lcs, e.backwardlcs(e.limit, kmax)...) + // These may overlap (e.forwardlcs and e.backwardlcs return sorted lcs) + ans := lcs.fix() + return ans +} + +// Does Myers' Lemma apply? +func (e *editGraph) twoDone(df, db int) (int, bool) { + if (df+db+e.delta)%2 != 0 { + return 0, false // diagonals cannot overlap + } + kmin := -db + e.delta + if -df > kmin { + kmin = -df + } + kmax := db + e.delta + if df < kmax { + kmax = df + } + for k := kmin; k <= kmax; k += 2 { + x := e.vf.get(df, k) + u := e.vb.get(db, k-e.delta) + if u <= x { + // is it worth looking at all the other k? + for l := k; l <= kmax; l += 2 { + x := e.vf.get(df, l) + y := x - l + u := e.vb.get(db, l-e.delta) + v := u - l + if x == u || u == 0 || v == 0 || y == e.uy || x == e.ux { + return l, true + } + } + return k, true + } + } + return 0, false +} + +func (e *editGraph) twolcs(df, db, kf int) lcs { + // db==df || db+1==df + x := e.vf.get(df, kf) + y := x - kf + kb := kf - e.delta + u := e.vb.get(db, kb) + v := u - kf + + // Myers proved there is a df-path from (0,0) to (u,v) + // and a db-path from (x,y) to (N,M). + // In the first case the overall path is the forward path + // to (u,v) followed by the backward path to (N,M). + // In the second case the path is the backward path to (x,y) + // followed by the forward path to (x,y) from (0,0). + + // Look for some special cases to avoid computing either of these paths. + if x == u { + // "babaab" "cccaba" + // already patched together + lcs := e.forwardlcs(df, kf) + lcs = append(lcs, e.backwardlcs(db, kb)...) + return lcs.sort() + } + + // is (u-1,v) or (u,v-1) labelled df-1? + // if so, that forward df-1-path plus a horizontal or vertical edge + // is the df-path to (u,v), then plus the db-path to (N,M) + if u > 0 && ok(df-1, u-1-v) && e.vf.get(df-1, u-1-v) == u-1 { + // "aabbab" "cbcabc" + lcs := e.forwardlcs(df-1, u-1-v) + lcs = append(lcs, e.backwardlcs(db, kb)...) + return lcs.sort() + } + if v > 0 && ok(df-1, (u-(v-1))) && e.vf.get(df-1, u-(v-1)) == u { + // "abaabb" "bcacab" + lcs := e.forwardlcs(df-1, u-(v-1)) + lcs = append(lcs, e.backwardlcs(db, kb)...) + return lcs.sort() + } + + // The path can't possibly contribute to the lcs because it + // is all horizontal or vertical edges + if u == 0 || v == 0 || x == e.ux || y == e.uy { + // "abaabb" "abaaaa" + if u == 0 || v == 0 { + return e.backwardlcs(db, kb) + } + return e.forwardlcs(df, kf) + } + + // is (x+1,y) or (x,y+1) labelled db-1? + if x+1 <= e.ux && ok(db-1, x+1-y-e.delta) && e.vb.get(db-1, x+1-y-e.delta) == x+1 { + // "bababb" "baaabb" + lcs := e.backwardlcs(db-1, kb+1) + lcs = append(lcs, e.forwardlcs(df, kf)...) + return lcs.sort() + } + if y+1 <= e.uy && ok(db-1, x-(y+1)-e.delta) && e.vb.get(db-1, x-(y+1)-e.delta) == x { + // "abbbaa" "cabacc" + lcs := e.backwardlcs(db-1, kb-1) + lcs = append(lcs, e.forwardlcs(df, kf)...) + return lcs.sort() + } + + // need to compute another path + // "aabbaa" "aacaba" + lcs := e.backwardlcs(db, kb) + oldx, oldy := e.ux, e.uy + e.ux = u + e.uy = v + lcs = append(lcs, e.forward()...) + e.ux, e.uy = oldx, oldy + return lcs.sort() +} diff --git a/internal/diff/lcs/old_test.go b/internal/diff/lcs/old_test.go new file mode 100644 index 00000000000..1d6047694f3 --- /dev/null +++ b/internal/diff/lcs/old_test.go @@ -0,0 +1,203 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package lcs + +import ( + "math/rand" + "testing" +) + +func TestForwardOld(t *testing.T) { + for _, tx := range Btests { + lim := len(tx.a) + len(tx.b) + left, right := []byte(tx.a), []byte(tx.b) + g := newegraph(left, right, lim) + lcs := g.forward() + diffs := g.fromlcs(lcs) + check(t, tx.a, lcs, tx.lcs) + checkDiffs(t, tx.a, diffs, tx.b) + + g = newegraph(right, left, lim) + lcs = g.forward() + diffs = g.fromlcs(lcs) + check(t, tx.b, lcs, tx.lcs) + checkDiffs(t, tx.b, diffs, tx.a) + } +} + +func TestBackwardOld(t *testing.T) { + for _, tx := range Btests { + lim := len(tx.a) + len(tx.b) + left, right := []byte(tx.a), []byte(tx.b) + g := newegraph(left, right, lim) + lcs := g.backward() + check(t, tx.a, lcs, tx.lcs) + diffs := g.fromlcs(lcs) + checkDiffs(t, tx.a, diffs, tx.b) + + g = newegraph(right, left, lim) + lcs = g.backward() + diffs = g.fromlcs(lcs) + check(t, tx.b, lcs, tx.lcs) + checkDiffs(t, tx.b, diffs, tx.a) + } +} + +func TestTwosidedOld(t *testing.T) { + // test both (a,b) and (b,a) + for _, tx := range Btests { + left, right := []byte(tx.a), []byte(tx.b) + lim := len(tx.a) + len(tx.b) + diffs, lcs := Compute(left, right, lim) + check(t, tx.a, lcs, tx.lcs) + checkDiffs(t, tx.a, diffs, tx.b) + diffs, lcs = Compute(right, left, lim) + check(t, tx.b, lcs, tx.lcs) + checkDiffs(t, tx.b, diffs, tx.a) + } +} + +func TestIntOld(t *testing.T) { + // need to avoid any characters in btests + lfill, rfill := "AAAAAAAAAAAA", "BBBBBBBBBBBB" + for _, tx := range Btests { + if len(tx.a) < 2 || len(tx.b) < 2 { + continue + } + left := []byte(tx.a + lfill) + right := []byte(tx.b + rfill) + lim := len(tx.a) + len(tx.b) + diffs, lcs := Compute(left, right, lim) + check(t, string(left), lcs, tx.lcs) + checkDiffs(t, string(left), diffs, string(right)) + diffs, lcs = Compute(right, left, lim) + check(t, string(right), lcs, tx.lcs) + checkDiffs(t, string(right), diffs, string(left)) + + left = []byte(lfill + tx.a) + right = []byte(rfill + tx.b) + diffs, lcs = Compute(left, right, lim) + check(t, string(left), lcs, tx.lcs) + checkDiffs(t, string(left), diffs, string(right)) + diffs, lcs = Compute(right, left, lim) + check(t, string(right), lcs, tx.lcs) + checkDiffs(t, string(right), diffs, string(left)) + } +} + +func TestSpecialOld(t *testing.T) { // needs lcs.fix + a := []byte("golang.org/x/tools/intern") + b := []byte("github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/intern") + diffs, lcs := Compute(a, b, 4) + if !lcs.valid() { + t.Errorf("%d,%v", len(diffs), lcs) + } +} + +func TestRegressionOld001(t *testing.T) { + a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" + + b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n" + for i := 1; i < len(b); i++ { + diffs, lcs := Compute([]byte(a), []byte(b), int(i)) // 14 from gopls + if !lcs.valid() { + t.Errorf("%d,%v", len(diffs), lcs) + } + checkDiffs(t, a, diffs, b) + } +} + +func TestRegressionOld002(t *testing.T) { + a := "n\"\n)\n" + b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n" + for i := 1; i <= len(b); i++ { + diffs, lcs := Compute([]byte(a), []byte(b), int(i)) + if !lcs.valid() { + t.Errorf("%d,%v", len(diffs), lcs) + } + checkDiffs(t, a, diffs, b) + } +} + +func TestRegressionOld003(t *testing.T) { + a := "golang.org/x/hello v1.0.0\nrequire golang.org/x/unused v1" + b := "golang.org/x/hello v1" + for i := 1; i <= len(a); i++ { + diffs, lcs := Compute([]byte(a), []byte(b), int(i)) + if !lcs.valid() { + t.Errorf("%d,%v", len(diffs), lcs) + } + checkDiffs(t, a, diffs, b) + } +} + +func TestRandOld(t *testing.T) { + rand.Seed(1) + for i := 0; i < 1000; i++ { + a := []rune(randstr("abω", 16)) + b := []rune(randstr("abωc", 16)) + g := newegraph(a, b, 24) // large enough to get true lcs + two := g.twosided() + forw := g.forward() + back := g.backward() + if lcslen(two) != lcslen(forw) || lcslen(forw) != lcslen(back) { + t.Logf("\n%v\n%v\n%v", forw, back, two) + t.Fatalf("%d forw:%d back:%d two:%d", i, lcslen(forw), lcslen(back), lcslen(two)) + } + if !two.valid() || !forw.valid() || !back.valid() { + t.Errorf("check failure") + } + } +} + +func BenchmarkTwoOld(b *testing.B) { + tests := genBench("abc", 96) + for i := 0; i < b.N; i++ { + for _, tt := range tests { + _, two := Compute([]byte(tt.before), []byte(tt.after), 100) + if !two.valid() { + b.Error("check failed") + } + } + } +} + +func BenchmarkForwOld(b *testing.B) { + tests := genBench("abc", 96) + for i := 0; i < b.N; i++ { + for _, tt := range tests { + _, two := Compute([]byte(tt.before), []byte(tt.after), 100) + if !two.valid() { + b.Error("check failed") + } + } + } +} + +func genBench(set string, n int) []struct{ before, after string } { + // before and after for benchmarks. 24 strings of length n with + // before and after differing at least once, and about 5% + rand.Seed(3) + var ans []struct{ before, after string } + for i := 0; i < 24; i++ { + // maybe b should have an approximately known number of diffs + a := randstr(set, n) + cnt := 0 + bb := make([]rune, 0, n) + for _, r := range a { + if rand.Float64() < .05 { + cnt++ + r = 'N' + } + bb = append(bb, r) + } + if cnt == 0 { + // avoid == shortcut + bb[n/2] = 'N' + } + ans = append(ans, struct{ before, after string }{a, string(bb)}) + } + return ans +} diff --git a/internal/lsp/diff/myers/diff.go b/internal/diff/myers/diff.go similarity index 84% rename from internal/lsp/diff/myers/diff.go rename to internal/diff/myers/diff.go index a59475058a5..7c2d4356b42 100644 --- a/internal/lsp/diff/myers/diff.go +++ b/internal/diff/myers/diff.go @@ -8,31 +8,41 @@ package myers import ( "strings" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/diff" ) // Sources: // https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/ // https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2 -func ComputeEdits(uri span.URI, before, after string) ([]diff.TextEdit, error) { - ops := operations(splitLines(before), splitLines(after)) - edits := make([]diff.TextEdit, 0, len(ops)) +func ComputeEdits(before, after string) []diff.Edit { + beforeLines := splitLines(before) + ops := operations(beforeLines, splitLines(after)) + + // Build a table mapping line number to offset. + lineOffsets := make([]int, 0, len(beforeLines)+1) + total := 0 + for i := range beforeLines { + lineOffsets = append(lineOffsets, total) + total += len(beforeLines[i]) + } + lineOffsets = append(lineOffsets, total) // EOF + + edits := make([]diff.Edit, 0, len(ops)) for _, op := range ops { - s := span.New(uri, span.NewPoint(op.I1+1, 1, 0), span.NewPoint(op.I2+1, 1, 0)) + start, end := lineOffsets[op.I1], lineOffsets[op.I2] switch op.Kind { case diff.Delete: - // Delete: unformatted[i1:i2] is deleted. - edits = append(edits, diff.TextEdit{Span: s}) + // Delete: before[I1:I2] is deleted. + edits = append(edits, diff.Edit{Start: start, End: end}) case diff.Insert: - // Insert: formatted[j1:j2] is inserted at unformatted[i1:i1]. + // Insert: after[J1:J2] is inserted at before[I1:I1]. if content := strings.Join(op.Content, ""); content != "" { - edits = append(edits, diff.TextEdit{Span: s, NewText: content}) + edits = append(edits, diff.Edit{Start: start, End: end, New: content}) } } } - return edits, nil + return edits } type operation struct { diff --git a/internal/lsp/diff/myers/diff_test.go b/internal/diff/myers/diff_test.go similarity index 74% rename from internal/lsp/diff/myers/diff_test.go rename to internal/diff/myers/diff_test.go index bce0399c58d..f244455586b 100644 --- a/internal/lsp/diff/myers/diff_test.go +++ b/internal/diff/myers/diff_test.go @@ -7,8 +7,8 @@ package myers_test import ( "testing" - "golang.org/x/tools/internal/lsp/diff/difftest" - "golang.org/x/tools/internal/lsp/diff/myers" + "golang.org/x/tools/internal/diff/difftest" + "golang.org/x/tools/internal/diff/myers" ) func TestDiff(t *testing.T) { diff --git a/internal/diff/ndiff.go b/internal/diff/ndiff.go new file mode 100644 index 00000000000..4dd83237af6 --- /dev/null +++ b/internal/diff/ndiff.go @@ -0,0 +1,112 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package diff + +import ( + "bytes" + "unicode/utf8" + + "golang.org/x/tools/internal/diff/lcs" +) + +// Strings computes the differences between two strings. +// The resulting edits respect rune boundaries. +func Strings(before, after string) []Edit { + if before == after { + return nil // common case + } + + if stringIsASCII(before) && stringIsASCII(after) { + return diffASCII([]byte(before), []byte(after)) + } + return diffRunes([]rune(before), []rune(after)) +} + +// Bytes computes the differences between two byte slices. +// The resulting edits respect rune boundaries. +func Bytes(before, after []byte) []Edit { + if bytes.Equal(before, after) { + return nil // common case + } + + if bytesIsASCII(before) && bytesIsASCII(after) { + return diffASCII(before, after) + } + return diffRunes(runes(before), runes(after)) +} + +func diffASCII(before, after []byte) []Edit { + diffs, _ := lcs.Compute(before, after, maxDiffs/2) + + // Convert from LCS diffs. + res := make([]Edit, len(diffs)) + for i, d := range diffs { + res[i] = Edit{d.Start, d.End, d.Text} + } + return res +} + +func diffRunes(before, after []rune) []Edit { + diffs, _ := lcs.Compute(before, after, maxDiffs/2) + + // The diffs returned by the lcs package use indexes + // into whatever slice was passed in. + // Convert rune offsets to byte offsets. + res := make([]Edit, len(diffs)) + lastEnd := 0 + utf8Len := 0 + for i, d := range diffs { + utf8Len += runesLen(before[lastEnd:d.Start]) // text between edits + start := utf8Len + utf8Len += runesLen(before[d.Start:d.End]) // text deleted by this edit + res[i] = Edit{start, utf8Len, d.Text} + lastEnd = d.End + } + return res +} + +// maxDiffs is a limit on how deeply the lcs algorithm should search +// the value is just a guess +const maxDiffs = 30 + +// runes is like []rune(string(bytes)) without the duplicate allocation. +func runes(bytes []byte) []rune { + n := utf8.RuneCount(bytes) + runes := make([]rune, n) + for i := 0; i < n; i++ { + r, sz := utf8.DecodeRune(bytes) + bytes = bytes[sz:] + runes[i] = r + } + return runes +} + +// runesLen returns the length in bytes of the UTF-8 encoding of runes. +func runesLen(runes []rune) (len int) { + for _, r := range runes { + len += utf8.RuneLen(r) + } + return len +} + +// stringIsASCII reports whether s contains only ASCII. +// TODO(adonovan): combine when x/tools allows generics. +func stringIsASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] >= utf8.RuneSelf { + return false + } + } + return true +} + +func bytesIsASCII(s []byte) bool { + for i := 0; i < len(s); i++ { + if s[i] >= utf8.RuneSelf { + return false + } + } + return true +} diff --git a/internal/lsp/diff/unified.go b/internal/diff/unified.go similarity index 55% rename from internal/lsp/diff/unified.go rename to internal/diff/unified.go index 323471d2046..fa376f17872 100644 --- a/internal/lsp/diff/unified.go +++ b/internal/diff/unified.go @@ -6,31 +6,56 @@ package diff import ( "fmt" + "log" "strings" ) -// Unified represents a set of edits as a unified diff. -type Unified struct { +// Unified returns a unified diff of the old and new strings. +// The old and new labels are the names of the old and new files. +// If the strings are equal, it returns the empty string. +func Unified(oldLabel, newLabel, old, new string) string { + edits := Strings(old, new) + unified, err := ToUnified(oldLabel, newLabel, old, edits) + if err != nil { + // Can't happen: edits are consistent. + log.Fatalf("internal error in diff.Unified: %v", err) + } + return unified +} + +// ToUnified applies the edits to content and returns a unified diff. +// The old and new labels are the names of the content and result files. +// It returns an error if the edits are inconsistent; see ApplyEdits. +func ToUnified(oldLabel, newLabel, content string, edits []Edit) (string, error) { + u, err := toUnified(oldLabel, newLabel, content, edits) + if err != nil { + return "", err + } + return u.String(), nil +} + +// unified represents a set of edits as a unified diff. +type unified struct { // From is the name of the original file. From string // To is the name of the modified file. To string // Hunks is the set of edit hunks needed to transform the file content. - Hunks []*Hunk + Hunks []*hunk } // Hunk represents a contiguous set of line edits to apply. -type Hunk struct { +type hunk struct { // The line in the original source where the hunk starts. FromLine int // The line in the original source where the hunk finishes. ToLine int // The set of line based edits to apply. - Lines []Line + Lines []line } // Line represents a single line operation to apply as part of a Hunk. -type Line struct { +type line struct { // Kind is the type of line this represents, deletion, insertion or copy. Kind OpKind // Content is the content of this line. @@ -40,6 +65,7 @@ type Line struct { } // OpKind is used to denote the type of operation a line represents. +// TODO(adonovan): hide this once the myers package no longer references it. type OpKind int const ( @@ -73,27 +99,34 @@ const ( gap = edge * 2 ) -// ToUnified takes a file contents and a sequence of edits, and calculates +// toUnified takes a file contents and a sequence of edits, and calculates // a unified diff that represents those edits. -func ToUnified(from, to string, content string, edits []TextEdit) Unified { - u := Unified{ - From: from, - To: to, +func toUnified(fromName, toName string, content string, edits []Edit) (unified, error) { + u := unified{ + From: fromName, + To: toName, } if len(edits) == 0 { - return u + return u, nil } - edits, partial := prepareEdits(content, edits) - if partial { - edits = lineEdits(content, edits) + var err error + edits, err = lineEdits(content, edits) // expand to whole lines + if err != nil { + return u, err } lines := splitLines(content) - var h *Hunk + var h *hunk last := 0 toLine := 0 for _, edit := range edits { - start := edit.Span.Start().Line() - 1 - end := edit.Span.End().Line() - 1 + // Compute the zero-based line numbers of the edit start and end. + // TODO(adonovan): opt: compute incrementally, avoid O(n^2). + start := strings.Count(content[:edit.Start], "\n") + end := strings.Count(content[:edit.End], "\n") + if edit.End == len(content) && len(content) > 0 && content[len(content)-1] != '\n' { + end++ // EOF counts as an implicit newline + } + switch { case h != nil && start == last: //direct extension @@ -108,7 +141,7 @@ func ToUnified(from, to string, content string, edits []TextEdit) Unified { u.Hunks = append(u.Hunks, h) } toLine += start - last - h = &Hunk{ + h = &hunk{ FromLine: start + 1, ToLine: toLine + 1, } @@ -119,12 +152,12 @@ func ToUnified(from, to string, content string, edits []TextEdit) Unified { } last = start for i := start; i < end; i++ { - h.Lines = append(h.Lines, Line{Kind: Delete, Content: lines[i]}) + h.Lines = append(h.Lines, line{Kind: Delete, Content: lines[i]}) last++ } - if edit.NewText != "" { - for _, line := range splitLines(edit.NewText) { - h.Lines = append(h.Lines, Line{Kind: Insert, Content: line}) + if edit.New != "" { + for _, content := range splitLines(edit.New) { + h.Lines = append(h.Lines, line{Kind: Insert, Content: content}) toLine++ } } @@ -134,7 +167,7 @@ func ToUnified(from, to string, content string, edits []TextEdit) Unified { addEqualLines(h, lines, last, last+edge) u.Hunks = append(u.Hunks, h) } - return u + return u, nil } func splitLines(text string) []string { @@ -145,7 +178,7 @@ func splitLines(text string) []string { return lines } -func addEqualLines(h *Hunk, lines []string, start, end int) int { +func addEqualLines(h *hunk, lines []string, start, end int) int { delta := 0 for i := start; i < end; i++ { if i < 0 { @@ -154,20 +187,21 @@ func addEqualLines(h *Hunk, lines []string, start, end int) int { if i >= len(lines) { return delta } - h.Lines = append(h.Lines, Line{Kind: Equal, Content: lines[i]}) + h.Lines = append(h.Lines, line{Kind: Equal, Content: lines[i]}) delta++ } return delta } -// Format converts a unified diff to the standard textual form for that diff. +// String converts a unified diff to the standard textual form for that diff. // The output of this function can be passed to tools like patch. -func (u Unified) Format(f fmt.State, r rune) { +func (u unified) String() string { if len(u.Hunks) == 0 { - return + return "" } - fmt.Fprintf(f, "--- %s\n", u.From) - fmt.Fprintf(f, "+++ %s\n", u.To) + b := new(strings.Builder) + fmt.Fprintf(b, "--- %s\n", u.From) + fmt.Fprintf(b, "+++ %s\n", u.To) for _, hunk := range u.Hunks { fromCount, toCount := 0, 0 for _, l := range hunk.Lines { @@ -181,30 +215,34 @@ func (u Unified) Format(f fmt.State, r rune) { toCount++ } } - fmt.Fprint(f, "@@") + fmt.Fprint(b, "@@") if fromCount > 1 { - fmt.Fprintf(f, " -%d,%d", hunk.FromLine, fromCount) + fmt.Fprintf(b, " -%d,%d", hunk.FromLine, fromCount) + } else if hunk.FromLine == 1 && fromCount == 0 { + // Match odd GNU diff -u behavior adding to empty file. + fmt.Fprintf(b, " -0,0") } else { - fmt.Fprintf(f, " -%d", hunk.FromLine) + fmt.Fprintf(b, " -%d", hunk.FromLine) } if toCount > 1 { - fmt.Fprintf(f, " +%d,%d", hunk.ToLine, toCount) + fmt.Fprintf(b, " +%d,%d", hunk.ToLine, toCount) } else { - fmt.Fprintf(f, " +%d", hunk.ToLine) + fmt.Fprintf(b, " +%d", hunk.ToLine) } - fmt.Fprint(f, " @@\n") + fmt.Fprint(b, " @@\n") for _, l := range hunk.Lines { switch l.Kind { case Delete: - fmt.Fprintf(f, "-%s", l.Content) + fmt.Fprintf(b, "-%s", l.Content) case Insert: - fmt.Fprintf(f, "+%s", l.Content) + fmt.Fprintf(b, "+%s", l.Content) default: - fmt.Fprintf(f, " %s", l.Content) + fmt.Fprintf(b, " %s", l.Content) } if !strings.HasSuffix(l.Content, "\n") { - fmt.Fprintf(f, "\n\\ No newline at end of file\n") + fmt.Fprintf(b, "\n\\ No newline at end of file\n") } } } + return b.String() } diff --git a/internal/lsp/debug/tag/tag.go b/internal/event/tag/tag.go similarity index 100% rename from internal/lsp/debug/tag/tag.go rename to internal/event/tag/tag.go diff --git a/go/analysis/internal/facts/facts.go b/internal/facts/facts.go similarity index 91% rename from go/analysis/internal/facts/facts.go rename to internal/facts/facts.go index 006abab84ef..81df45161a8 100644 --- a/go/analysis/internal/facts/facts.go +++ b/internal/facts/facts.go @@ -152,6 +152,23 @@ type gobFact struct { Fact analysis.Fact // type and value of user-defined Fact } +// A Decoder decodes the facts from the direct imports of the package +// provided to NewEncoder. A single decoder may be used to decode +// multiple fact sets (e.g. each for a different set of fact types) +// for the same package. Each call to Decode returns an independent +// fact set. +type Decoder struct { + pkg *types.Package + packages map[string]*types.Package +} + +// NewDecoder returns a fact decoder for the specified package. +func NewDecoder(pkg *types.Package) *Decoder { + // Compute the import map for this package. + // See the package doc comment. + return &Decoder{pkg, importMap(pkg.Imports())} +} + // Decode decodes all the facts relevant to the analysis of package pkg. // The read function reads serialized fact data from an external source // for one of of pkg's direct imports. The empty file is a valid @@ -159,28 +176,24 @@ type gobFact struct { // // It is the caller's responsibility to call gob.Register on all // necessary fact types. -func Decode(pkg *types.Package, read func(packagePath string) ([]byte, error)) (*Set, error) { - // Compute the import map for this package. - // See the package doc comment. - packages := importMap(pkg.Imports()) - +func (d *Decoder) Decode(read func(*types.Package) ([]byte, error)) (*Set, error) { // Read facts from imported packages. // Facts may describe indirectly imported packages, or their objects. m := make(map[key]analysis.Fact) // one big bucket - for _, imp := range pkg.Imports() { + for _, imp := range d.pkg.Imports() { logf := func(format string, args ...interface{}) { if debug { prefix := fmt.Sprintf("in %s, importing %s: ", - pkg.Path(), imp.Path()) + d.pkg.Path(), imp.Path()) log.Print(prefix, fmt.Sprintf(format, args...)) } } // Read the gob-encoded facts. - data, err := read(imp.Path()) + data, err := read(imp) if err != nil { return nil, fmt.Errorf("in %s, can't import facts for package %q: %v", - pkg.Path(), imp.Path(), err) + d.pkg.Path(), imp.Path(), err) } if len(data) == 0 { continue // no facts @@ -195,7 +208,7 @@ func Decode(pkg *types.Package, read func(packagePath string) ([]byte, error)) ( // Parse each one into a key and a Fact. for _, f := range gobFacts { - factPkg := packages[f.PkgPath] + factPkg := d.packages[f.PkgPath] if factPkg == nil { // Fact relates to a dependency that was // unused in this translation unit. Skip. @@ -222,7 +235,7 @@ func Decode(pkg *types.Package, read func(packagePath string) ([]byte, error)) ( } } - return &Set{pkg: pkg, m: m}, nil + return &Set{pkg: d.pkg, m: m}, nil } // Encode encodes a set of facts to a memory buffer. diff --git a/go/analysis/internal/facts/facts_test.go b/internal/facts/facts_test.go similarity index 96% rename from go/analysis/internal/facts/facts_test.go rename to internal/facts/facts_test.go index a55e30d7a31..5c7b12ef1d4 100644 --- a/go/analysis/internal/facts/facts_test.go +++ b/internal/facts/facts_test.go @@ -14,8 +14,8 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/go/analysis/internal/facts" "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/facts" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" ) @@ -148,7 +148,7 @@ func TestEncodeDecode(t *testing.T) { type N4[T a.T4|int8] func() T type N5[T interface{Bar() a.T5} ] func() T - type t5 struct{}; func (t5) Bar() a.T5 + type t5 struct{}; func (t5) Bar() a.T5 { return 0 } var G1 N1[a.T1] var G2 func() N2[a.T2] @@ -216,7 +216,7 @@ type pkgLookups struct { // are passed during analysis. It operates on a group of Go file contents. Then // for each in tests it does the following: // 1. loads and type checks the package, -// 2. calls facts.Decode to loads the facts exported by its imports, +// 2. calls (*facts.Decoder).Decode to load the facts exported by its imports, // 3. exports a myFact Fact for all of package level objects, // 4. For each lookup for the current package: // 4.a) lookup the types.Object for an Go source expression in the curent package @@ -239,7 +239,7 @@ func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) // factmap represents the passing of encoded facts from one // package to another. In practice one would use the file system. factmap := make(map[string][]byte) - read := func(path string) ([]byte, error) { return factmap[path], nil } + read := func(imp *types.Package) ([]byte, error) { return factmap[imp.Path()], nil } // Analyze packages in order, look up various objects accessible within // each package, and see if they have a fact. The "analysis" exports a @@ -255,7 +255,7 @@ func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) } // decode - facts, err := facts.Decode(pkg, read) + facts, err := facts.NewDecoder(pkg).Decode(read) if err != nil { t.Fatalf("Decode failed: %v", err) } @@ -357,7 +357,7 @@ func TestFactFilter(t *testing.T) { } obj := pkg.Scope().Lookup("A") - s, err := facts.Decode(pkg, func(string) ([]byte, error) { return nil, nil }) + s, err := facts.NewDecoder(pkg).Decode(func(*types.Package) ([]byte, error) { return nil, nil }) if err != nil { t.Fatal(err) } diff --git a/go/analysis/internal/facts/imports.go b/internal/facts/imports.go similarity index 95% rename from go/analysis/internal/facts/imports.go rename to internal/facts/imports.go index 8a5553e2e9b..a3aa90dd1c5 100644 --- a/go/analysis/internal/facts/imports.go +++ b/internal/facts/imports.go @@ -20,6 +20,9 @@ import ( // // Packages in the map that are only indirectly imported may be // incomplete (!pkg.Complete()). +// +// TODO(adonovan): opt: compute this information more efficiently +// by obtaining it from the internals of the gcexportdata decoder. func importMap(imports []*types.Package) map[string]*types.Package { objects := make(map[types.Object]bool) packages := make(map[string]*types.Package) diff --git a/internal/fastwalk/fastwalk_darwin.go b/internal/fastwalk/fastwalk_darwin.go new file mode 100644 index 00000000000..0ca55e0d56f --- /dev/null +++ b/internal/fastwalk/fastwalk_darwin.go @@ -0,0 +1,119 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && cgo +// +build darwin,cgo + +package fastwalk + +/* +#include + +// fastwalk_readdir_r wraps readdir_r so that we don't have to pass a dirent** +// result pointer which triggers CGO's "Go pointer to Go pointer" check unless +// we allocat the result dirent* with malloc. +// +// fastwalk_readdir_r returns 0 on success, -1 upon reaching the end of the +// directory, or a positive error number to indicate failure. +static int fastwalk_readdir_r(DIR *fd, struct dirent *entry) { + struct dirent *result; + int ret = readdir_r(fd, entry, &result); + if (ret == 0 && result == NULL) { + ret = -1; // EOF + } + return ret; +} +*/ +import "C" + +import ( + "os" + "syscall" + "unsafe" +) + +func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error { + fd, err := openDir(dirName) + if err != nil { + return &os.PathError{Op: "opendir", Path: dirName, Err: err} + } + defer C.closedir(fd) + + skipFiles := false + var dirent syscall.Dirent + for { + ret := int(C.fastwalk_readdir_r(fd, (*C.struct_dirent)(unsafe.Pointer(&dirent)))) + if ret != 0 { + if ret == -1 { + break // EOF + } + if ret == int(syscall.EINTR) { + continue + } + return &os.PathError{Op: "readdir", Path: dirName, Err: syscall.Errno(ret)} + } + if dirent.Ino == 0 { + continue + } + typ := dtToType(dirent.Type) + if skipFiles && typ.IsRegular() { + continue + } + name := (*[len(syscall.Dirent{}.Name)]byte)(unsafe.Pointer(&dirent.Name))[:] + name = name[:dirent.Namlen] + for i, c := range name { + if c == 0 { + name = name[:i] + break + } + } + // Check for useless names before allocating a string. + if string(name) == "." || string(name) == ".." { + continue + } + if err := fn(dirName, string(name), typ); err != nil { + if err != ErrSkipFiles { + return err + } + skipFiles = true + } + } + + return nil +} + +func dtToType(typ uint8) os.FileMode { + switch typ { + case syscall.DT_BLK: + return os.ModeDevice + case syscall.DT_CHR: + return os.ModeDevice | os.ModeCharDevice + case syscall.DT_DIR: + return os.ModeDir + case syscall.DT_FIFO: + return os.ModeNamedPipe + case syscall.DT_LNK: + return os.ModeSymlink + case syscall.DT_REG: + return 0 + case syscall.DT_SOCK: + return os.ModeSocket + } + return ^os.FileMode(0) +} + +// openDir wraps opendir(3) and handles any EINTR errors. The returned *DIR +// needs to be closed with closedir(3). +func openDir(path string) (*C.DIR, error) { + name, err := syscall.BytePtrFromString(path) + if err != nil { + return nil, err + } + for { + fd, err := C.opendir((*C.char)(unsafe.Pointer(name))) + if err != syscall.EINTR { + return fd, err + } + } +} diff --git a/internal/fastwalk/fastwalk_dirent_ino.go b/internal/fastwalk/fastwalk_dirent_ino.go index ea02b9ebfe8..d3922890b0b 100644 --- a/internal/fastwalk/fastwalk_dirent_ino.go +++ b/internal/fastwalk/fastwalk_dirent_ino.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (linux || darwin) && !appengine -// +build linux darwin +//go:build (linux || (darwin && !cgo)) && !appengine +// +build linux darwin,!cgo // +build !appengine package fastwalk @@ -11,5 +11,5 @@ package fastwalk import "syscall" func direntInode(dirent *syscall.Dirent) uint64 { - return uint64(dirent.Ino) + return dirent.Ino } diff --git a/internal/fastwalk/fastwalk_dirent_namlen_bsd.go b/internal/fastwalk/fastwalk_dirent_namlen_bsd.go index d5c9c321ed2..38a4db6af3a 100644 --- a/internal/fastwalk/fastwalk_dirent_namlen_bsd.go +++ b/internal/fastwalk/fastwalk_dirent_namlen_bsd.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build darwin || freebsd || openbsd || netbsd -// +build darwin freebsd openbsd netbsd +//go:build (darwin && !cgo) || freebsd || openbsd || netbsd +// +build darwin,!cgo freebsd openbsd netbsd package fastwalk diff --git a/internal/fastwalk/fastwalk_unix.go b/internal/fastwalk/fastwalk_unix.go index 58bd87841e1..f12f1a734cc 100644 --- a/internal/fastwalk/fastwalk_unix.go +++ b/internal/fastwalk/fastwalk_unix.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (linux || darwin || freebsd || openbsd || netbsd) && !appengine -// +build linux darwin freebsd openbsd netbsd +//go:build (linux || freebsd || openbsd || netbsd || (darwin && !cgo)) && !appengine +// +build linux freebsd openbsd netbsd darwin,!cgo // +build !appengine package fastwalk diff --git a/internal/lsp/fuzzy/input.go b/internal/fuzzy/input.go similarity index 100% rename from internal/lsp/fuzzy/input.go rename to internal/fuzzy/input.go diff --git a/internal/lsp/fuzzy/input_test.go b/internal/fuzzy/input_test.go similarity index 98% rename from internal/lsp/fuzzy/input_test.go rename to internal/fuzzy/input_test.go index 0228347e4f0..64f66e363b8 100644 --- a/internal/lsp/fuzzy/input_test.go +++ b/internal/fuzzy/input_test.go @@ -9,7 +9,7 @@ import ( "sort" "testing" - "golang.org/x/tools/internal/lsp/fuzzy" + "golang.org/x/tools/internal/fuzzy" ) var rolesTests = []struct { diff --git a/internal/lsp/fuzzy/matcher.go b/internal/fuzzy/matcher.go similarity index 93% rename from internal/lsp/fuzzy/matcher.go rename to internal/fuzzy/matcher.go index 265cdcf1604..c0efd30dd9a 100644 --- a/internal/lsp/fuzzy/matcher.go +++ b/internal/fuzzy/matcher.go @@ -405,3 +405,30 @@ func (m *Matcher) poorMatch() bool { } return false } + +// BestMatch returns the name most similar to the +// pattern, using fuzzy matching, or the empty string. +func BestMatch(pattern string, names []string) string { + fuzz := NewMatcher(pattern) + best := "" + highScore := float32(0) // minimum score is 0 (no match) + for _, name := range names { + // TODO: Improve scoring algorithm. + score := fuzz.Score(name) + if score > highScore { + highScore = score + best = name + } else if score == 0 { + // Order matters in the fuzzy matching algorithm. If we find no match + // when matching the target to the identifier, try matching the identifier + // to the target. + revFuzz := NewMatcher(name) + revScore := revFuzz.Score(pattern) + if revScore > highScore { + highScore = revScore + best = name + } + } + } + return best +} diff --git a/internal/lsp/fuzzy/matcher_test.go b/internal/fuzzy/matcher_test.go similarity index 99% rename from internal/lsp/fuzzy/matcher_test.go rename to internal/fuzzy/matcher_test.go index 132ab5c800a..528224bd98d 100644 --- a/internal/lsp/fuzzy/matcher_test.go +++ b/internal/fuzzy/matcher_test.go @@ -13,7 +13,7 @@ import ( "math" "testing" - "golang.org/x/tools/internal/lsp/fuzzy" + "golang.org/x/tools/internal/fuzzy" ) type comparator struct { diff --git a/internal/lsp/fuzzy/symbol.go b/internal/fuzzy/symbol.go similarity index 100% rename from internal/lsp/fuzzy/symbol.go rename to internal/fuzzy/symbol.go diff --git a/internal/lsp/fuzzy/symbol_test.go b/internal/fuzzy/symbol_test.go similarity index 97% rename from internal/lsp/fuzzy/symbol_test.go rename to internal/fuzzy/symbol_test.go index cb28160dedb..df74bbe0d37 100644 --- a/internal/lsp/fuzzy/symbol_test.go +++ b/internal/fuzzy/symbol_test.go @@ -7,7 +7,7 @@ package fuzzy_test import ( "testing" - . "golang.org/x/tools/internal/lsp/fuzzy" + . "golang.org/x/tools/internal/fuzzy" ) func TestSymbolMatchIndex(t *testing.T) { diff --git a/go/internal/gcimporter/bexport.go b/internal/gcimporter/bexport.go similarity index 99% rename from go/internal/gcimporter/bexport.go rename to internal/gcimporter/bexport.go index 196cb3f9b41..30582ed6d3d 100644 --- a/go/internal/gcimporter/bexport.go +++ b/internal/gcimporter/bexport.go @@ -12,7 +12,6 @@ import ( "bytes" "encoding/binary" "fmt" - "go/ast" "go/constant" "go/token" "go/types" @@ -145,7 +144,7 @@ func BExportData(fset *token.FileSet, pkg *types.Package) (b []byte, err error) objcount := 0 scope := pkg.Scope() for _, name := range scope.Names() { - if !ast.IsExported(name) { + if !token.IsExported(name) { continue } if trace { @@ -482,7 +481,7 @@ func (p *exporter) method(m *types.Func) { p.pos(m) p.string(m.Name()) - if m.Name() != "_" && !ast.IsExported(m.Name()) { + if m.Name() != "_" && !token.IsExported(m.Name()) { p.pkg(m.Pkg(), false) } @@ -501,7 +500,7 @@ func (p *exporter) fieldName(f *types.Var) { // 3) field name doesn't match base type name (alias name) bname := basetypeName(f.Type()) if name == bname { - if ast.IsExported(name) { + if token.IsExported(name) { name = "" // 1) we don't need to know the field name or package } else { name = "?" // 2) use unexported name "?" to force package export @@ -514,7 +513,7 @@ func (p *exporter) fieldName(f *types.Var) { } p.string(name) - if name != "" && !ast.IsExported(name) { + if name != "" && !token.IsExported(name) { p.pkg(f.Pkg(), false) } } diff --git a/go/internal/gcimporter/bexport_test.go b/internal/gcimporter/bexport_test.go similarity index 99% rename from go/internal/gcimporter/bexport_test.go rename to internal/gcimporter/bexport_test.go index 3da5397eb50..b5e9ce10044 100644 --- a/go/internal/gcimporter/bexport_test.go +++ b/internal/gcimporter/bexport_test.go @@ -21,8 +21,8 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/buildutil" - "golang.org/x/tools/go/internal/gcimporter" "golang.org/x/tools/go/loader" + "golang.org/x/tools/internal/gcimporter" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typeparams/genericfeatures" ) @@ -109,7 +109,7 @@ type UnknownType undefined // Compare the packages' corresponding members. for _, name := range pkg.Scope().Names() { - if !ast.IsExported(name) { + if !token.IsExported(name) { continue } obj1 := pkg.Scope().Lookup(name) diff --git a/go/internal/gcimporter/bimport.go b/internal/gcimporter/bimport.go similarity index 100% rename from go/internal/gcimporter/bimport.go rename to internal/gcimporter/bimport.go diff --git a/go/internal/gcimporter/exportdata.go b/internal/gcimporter/exportdata.go similarity index 100% rename from go/internal/gcimporter/exportdata.go rename to internal/gcimporter/exportdata.go diff --git a/go/internal/gcimporter/gcimporter.go b/internal/gcimporter/gcimporter.go similarity index 94% rename from go/internal/gcimporter/gcimporter.go rename to internal/gcimporter/gcimporter.go index 493bfa03b0f..f8369cdc52e 100644 --- a/go/internal/gcimporter/gcimporter.go +++ b/internal/gcimporter/gcimporter.go @@ -9,7 +9,7 @@ // Package gcimporter provides various functions for reading // gc-generated object files that can be used to implement the // Importer interface defined by the Go 1.5 standard library package. -package gcimporter // import "golang.org/x/tools/go/internal/gcimporter" +package gcimporter // import "golang.org/x/tools/internal/gcimporter" import ( "bufio" @@ -22,11 +22,14 @@ import ( "io" "io/ioutil" "os" + "path" "path/filepath" "sort" "strconv" "strings" "text/scanner" + + "golang.org/x/tools/internal/goroot" ) const ( @@ -38,6 +41,25 @@ const ( trace = false ) +func lookupGorootExport(pkgpath, srcRoot, srcDir string) (string, bool) { + pkgpath = filepath.ToSlash(pkgpath) + m, err := goroot.PkgfileMap() + if err != nil { + return "", false + } + if export, ok := m[pkgpath]; ok { + return export, true + } + vendorPrefix := "vendor" + if strings.HasPrefix(srcDir, filepath.Join(srcRoot, "cmd")) { + vendorPrefix = path.Join("cmd", vendorPrefix) + } + pkgpath = path.Join(vendorPrefix, pkgpath) + fmt.Fprintln(os.Stderr, "looking up ", pkgpath) + export, ok := m[pkgpath] + return export, ok +} + var pkgExts = [...]string{".a", ".o"} // FindPkg returns the filename and unique package id for an import @@ -60,11 +82,18 @@ func FindPkg(path, srcDir string) (filename, id string) { } bp, _ := build.Import(path, srcDir, build.FindOnly|build.AllowBinary) if bp.PkgObj == "" { - id = path // make sure we have an id to print in error message - return + var ok bool + if bp.Goroot { + filename, ok = lookupGorootExport(path, bp.SrcRoot, srcDir) + } + 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 } - noext = strings.TrimSuffix(bp.PkgObj, ".a") - id = bp.ImportPath case build.IsLocalImport(path): // "./x" -> "/this/directory/x.ext", "/this/directory/x" @@ -85,6 +114,12 @@ func FindPkg(path, srcDir string) (filename, id string) { } } + if filename != "" { + if f, err := os.Stat(filename); err == nil && !f.IsDir() { + return + } + } + // try extensions for _, ext := range pkgExts { filename = noext + ext @@ -181,8 +216,9 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func defer rc.Close() var hdr string + var size int64 buf := bufio.NewReader(rc) - if hdr, _, err = FindExportData(buf); err != nil { + if hdr, size, err = FindExportData(buf); err != nil { return } @@ -210,10 +246,27 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func // The indexed export format starts with an 'i'; the older // binary export format starts with a 'c', 'd', or 'v' // (from "version"). Select appropriate importer. - if len(data) > 0 && data[0] == 'i' { - _, pkg, err = IImportData(fset, packages, data[1:], id) - } else { - _, pkg, err = BImportData(fset, packages, data, id) + if len(data) > 0 { + switch data[0] { + case 'i': + _, pkg, err := IImportData(fset, packages, data[1:], id) + return pkg, err + + case 'v', 'c', 'd': + _, pkg, err := BImportData(fset, packages, data, id) + return pkg, err + + case 'u': + _, 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) + } } default: diff --git a/go/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go similarity index 77% rename from go/internal/gcimporter/gcimporter_test.go rename to internal/gcimporter/gcimporter_test.go index 4e992af76b3..e4029c0d5e1 100644 --- a/go/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -10,8 +10,12 @@ package gcimporter import ( "bytes" "fmt" + "go/ast" "go/build" "go/constant" + goimporter "go/importer" + goparser "go/parser" + "go/token" "go/types" "io/ioutil" "os" @@ -44,16 +48,26 @@ func needsCompiler(t *testing.T, compiler string) { // compile runs the compiler on filename, with dirname as the working directory, // and writes the output file to outdirname. -func compile(t *testing.T, dirname, filename, outdirname string) string { +// compile gives the resulting package a packagepath of p. +func compile(t *testing.T, dirname, filename, outdirname string, packagefiles map[string]string) string { + return compilePkg(t, dirname, filename, outdirname, packagefiles, "p") +} + +func compilePkg(t *testing.T, dirname, filename, outdirname string, packagefiles map[string]string, pkg string) string { testenv.NeedsGoBuild(t) // filename must end with ".go" - if !strings.HasSuffix(filename, ".go") { + basename := strings.TrimSuffix(filepath.Base(filename), ".go") + ok := filename != basename + if !ok { t.Fatalf("filename doesn't end in .go: %s", filename) } - basename := filepath.Base(filename) - outname := filepath.Join(outdirname, basename[:len(basename)-2]+"o") - cmd := exec.Command("go", "tool", "compile", "-p=p", "-o", outname, filename) + objname := basename + ".o" + outname := filepath.Join(outdirname, objname) + importcfgfile := filepath.Join(outdirname, basename) + ".importcfg" + testenv.WriteImportcfg(t, importcfgfile, packagefiles) + importreldir := strings.ReplaceAll(outdirname, string(os.PathSeparator), "/") + cmd := exec.Command("go", "tool", "compile", "-p", pkg, "-D", importreldir, "-importcfg", importcfgfile, "-o", outname, filename) cmd.Dir = dirname out, err := cmd.CombinedOutput() if err != nil { @@ -125,7 +139,7 @@ func TestImportTestdata(t *testing.T) { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) - compile(t, "testdata", testfile, filepath.Join(tmpdir, "testdata")) + compile(t, "testdata", testfile, filepath.Join(tmpdir, "testdata"), nil) // filename should end with ".go" filename := testfile[:len(testfile)-3] @@ -140,7 +154,11 @@ func TestImportTestdata(t *testing.T) { // For now, we just test the presence of a few packages // that we know are there for sure. got := fmt.Sprint(pkg.Imports()) - for _, want := range []string{"go/ast", "go/token"} { + wants := []string{"go/ast", "go/token"} + if unifiedIR { + wants = []string{"go/ast"} + } + for _, want := range wants { if !strings.Contains(got, want) { t.Errorf(`Package("exports").Imports() = %s, does not contain %s`, got, want) } @@ -148,6 +166,129 @@ func TestImportTestdata(t *testing.T) { } } +func TestImportTypeparamTests(t *testing.T) { + testenv.NeedsGo1Point(t, 18) // requires generics + + // This package only handles gc export data. + if runtime.Compiler != "gc" { + t.Skipf("gc-built packages not available (compiler = %s)", runtime.Compiler) + } + + tmpdir := mktmpdir(t) + defer os.RemoveAll(tmpdir) + + // Check go files in test/typeparam, except those that fail for a known + // reason. + rootDir := filepath.Join(runtime.GOROOT(), "test", "typeparam") + list, err := os.ReadDir(rootDir) + if err != nil { + t.Fatal(err) + } + + var skip map[string]string + if !unifiedIR { + // The Go 1.18 frontend still fails several cases. + skip = map[string]string{ + "equal.go": "inconsistent embedded sorting", // TODO(rfindley): investigate this. + "nested.go": "fails to compile", // TODO(rfindley): investigate this. + "issue47631.go": "can not handle local type declarations", + "issue55101.go": "fails to compile", + } + } + + for _, entry := range list { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { + // For now, only consider standalone go files. + continue + } + + t.Run(entry.Name(), func(t *testing.T) { + if reason, ok := skip[entry.Name()]; ok { + t.Skip(reason) + } + + filename := filepath.Join(rootDir, entry.Name()) + src, err := os.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(src, []byte("// run")) && !bytes.HasPrefix(src, []byte("// compile")) { + // We're bypassing the logic of run.go here, so be conservative about + // the files we consider in an attempt to make this test more robust to + // changes in test/typeparams. + t.Skipf("not detected as a run test") + } + + // Compile and import, and compare the resulting package with the package + // that was type-checked directly. + compile(t, rootDir, entry.Name(), filepath.Join(tmpdir, "testdata"), nil) + pkgName := strings.TrimSuffix(entry.Name(), ".go") + imported := importPkg(t, "./testdata/"+pkgName, tmpdir) + checked := checkFile(t, filename, src) + + seen := make(map[string]bool) + for _, name := range imported.Scope().Names() { + if !token.IsExported(name) { + continue // ignore synthetic names like .inittask and .dict.* + } + seen[name] = true + + importedObj := imported.Scope().Lookup(name) + got := types.ObjectString(importedObj, types.RelativeTo(imported)) + got = sanitizeObjectString(got) + + checkedObj := checked.Scope().Lookup(name) + if checkedObj == nil { + t.Fatalf("imported object %q was not type-checked", name) + } + want := types.ObjectString(checkedObj, types.RelativeTo(checked)) + want = sanitizeObjectString(want) + + if got != want { + t.Errorf("imported %q as %q, want %q", name, got, want) + } + } + + for _, name := range checked.Scope().Names() { + if !token.IsExported(name) || seen[name] { + continue + } + t.Errorf("did not import object %q", name) + } + }) + } +} + +// sanitizeObjectString removes type parameter debugging markers from an object +// string, to normalize it for comparison. +// TODO(rfindley): this should not be necessary. +func sanitizeObjectString(s string) string { + var runes []rune + for _, r := range s { + if '₀' <= r && r < '₀'+10 { + continue // trim type parameter subscripts + } + runes = append(runes, r) + } + return string(runes) +} + +func checkFile(t *testing.T, filename string, src []byte) *types.Package { + fset := token.NewFileSet() + f, err := goparser.ParseFile(fset, filename, src, 0) + if err != nil { + t.Fatal(err) + } + config := types.Config{ + Importer: goimporter.Default(), + } + pkg, err := config.Check("", fset, []*ast.File{f}, nil) + if err != nil { + t.Fatal(err) + } + return pkg +} + func TestVersionHandling(t *testing.T) { if debug { t.Skip("TestVersionHandling panics in debug mode") @@ -451,8 +592,8 @@ func TestIssue13566(t *testing.T) { if err != nil { t.Fatal(err) } - compile(t, "testdata", "a.go", testoutdir) - compile(t, testoutdir, bpath, testoutdir) + compilePkg(t, "testdata", "a.go", testoutdir, nil, apkg(testoutdir)) + compile(t, testoutdir, bpath, testoutdir, map[string]string{apkg(testoutdir): filepath.Join(testoutdir, "a.o")}) // import must succeed (test for issue at hand) pkg := importPkg(t, "./testdata/b", tmpdir) @@ -520,7 +661,7 @@ func TestIssue15517(t *testing.T) { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) - compile(t, "testdata", "p.go", filepath.Join(tmpdir, "testdata")) + compile(t, "testdata", "p.go", filepath.Join(tmpdir, "testdata"), nil) // Multiple imports of p must succeed without redeclaration errors. // We use an import path that's not cleaned up so that the eventual @@ -611,13 +752,22 @@ func TestIssue51836(t *testing.T) { if err != nil { t.Fatal(err) } - compile(t, dir, "a.go", testoutdir) - compile(t, testoutdir, bpath, testoutdir) + compilePkg(t, dir, "a.go", testoutdir, nil, apkg(testoutdir)) + compile(t, testoutdir, bpath, testoutdir, map[string]string{apkg(testoutdir): filepath.Join(testoutdir, "a.o")}) // import must succeed (test for issue at hand) _ = importPkg(t, "./testdata/aa", tmpdir) } +// apkg returns the package "a" prefixed by (as a package) testoutdir +func apkg(testoutdir string) string { + apkg := testoutdir + "/a" + if os.PathSeparator != '/' { + apkg = strings.ReplaceAll(apkg, string(os.PathSeparator), "/") + } + return apkg +} + func importPkg(t *testing.T, path, srcDir string) *types.Package { pkg, err := Import(make(map[string]*types.Package), path, srcDir, nil) if err != nil { @@ -629,7 +779,7 @@ func importPkg(t *testing.T, path, srcDir string) *types.Package { func compileAndImportPkg(t *testing.T, name string) *types.Package { tmpdir := mktmpdir(t) defer os.RemoveAll(tmpdir) - compile(t, "testdata", name+".go", filepath.Join(tmpdir, "testdata")) + compile(t, "testdata", name+".go", filepath.Join(tmpdir, "testdata"), nil) return importPkg(t, "./testdata/"+name, tmpdir) } diff --git a/go/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go similarity index 90% rename from go/internal/gcimporter/iexport.go rename to internal/gcimporter/iexport.go index 9a4ff329e12..7d90f00f323 100644 --- a/go/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -12,7 +12,6 @@ import ( "bytes" "encoding/binary" "fmt" - "go/ast" "go/constant" "go/token" "go/types" @@ -26,6 +25,41 @@ import ( "golang.org/x/tools/internal/typeparams" ) +// IExportShallow encodes "shallow" export data for the specified package. +// +// No promises are made about the encoding other than that it can be +// decoded by the same version of IIExportShallow. If you plan to save +// export data in the file system, be sure to include a cryptographic +// digest of the executable in the key to avoid version skew. +func IExportShallow(fset *token.FileSet, pkg *types.Package) ([]byte, error) { + // In principle this operation can only fail if out.Write fails, + // but that's impossible for bytes.Buffer---and as a matter of + // fact iexportCommon doesn't even check for I/O errors. + // TODO(adonovan): handle I/O errors properly. + // TODO(adonovan): use byte slices throughout, avoiding copying. + const bundle, shallow = false, true + var out bytes.Buffer + err := iexportCommon(&out, fset, bundle, shallow, iexportVersion, []*types.Package{pkg}) + return out.Bytes(), err +} + +// IImportShallow decodes "shallow" types.Package data encoded by IExportShallow +// in the same executable. This function cannot import data from +// cmd/compile or gcexportdata.Write. +func IImportShallow(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string, insert InsertType) (*types.Package, error) { + const bundle = false + pkgs, err := iimportCommon(fset, imports, data, bundle, path, insert) + if err != nil { + return nil, err + } + return pkgs[0], nil +} + +// InsertType is the type of a function that creates a types.TypeName +// object for a named type and inserts it into the scope of the +// specified Package. +type InsertType = func(pkg *types.Package, name string) + // Current bundled export format version. Increase with each format change. // 0: initial implementation const bundleVersion = 0 @@ -36,15 +70,17 @@ const bundleVersion = 0 // The package path of the top-level package will not be recorded, // so that calls to IImportData can override with a provided package path. func IExportData(out io.Writer, fset *token.FileSet, pkg *types.Package) error { - return iexportCommon(out, fset, false, iexportVersion, []*types.Package{pkg}) + const bundle, shallow = false, false + return iexportCommon(out, fset, bundle, shallow, iexportVersion, []*types.Package{pkg}) } // IExportBundle writes an indexed export bundle for pkgs to out. func IExportBundle(out io.Writer, fset *token.FileSet, pkgs []*types.Package) error { - return iexportCommon(out, fset, true, iexportVersion, pkgs) + const bundle, shallow = true, false + return iexportCommon(out, fset, bundle, shallow, iexportVersion, pkgs) } -func iexportCommon(out io.Writer, fset *token.FileSet, bundle bool, version int, pkgs []*types.Package) (err error) { +func iexportCommon(out io.Writer, fset *token.FileSet, bundle, shallow bool, version int, pkgs []*types.Package) (err error) { if !debug { defer func() { if e := recover(); e != nil { @@ -61,6 +97,7 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle bool, version int, p := iexporter{ fset: fset, version: version, + shallow: shallow, allPkgs: map[*types.Package]bool{}, stringIndex: map[string]uint64{}, declIndex: map[types.Object]uint64{}, @@ -82,7 +119,7 @@ func iexportCommon(out io.Writer, fset *token.FileSet, bundle bool, version int, for _, pkg := range pkgs { scope := pkg.Scope() for _, name := range scope.Names() { - if ast.IsExported(name) { + if token.IsExported(name) { p.pushDecl(scope.Lookup(name)) } } @@ -205,7 +242,8 @@ type iexporter struct { out *bytes.Buffer version int - localpkg *types.Package + shallow bool // don't put types from other packages in the index + localpkg *types.Package // (nil in bundle mode) // allPkgs tracks all packages that have been referenced by // the export data, so we can ensure to include them in the @@ -256,6 +294,11 @@ func (p *iexporter) pushDecl(obj types.Object) { panic("cannot export package unsafe") } + // Shallow export data: don't index decls from other packages. + if p.shallow && obj.Pkg() != p.localpkg { + return + } + if _, ok := p.declIndex[obj]; ok { return } @@ -497,7 +540,7 @@ func (w *exportWriter) pkg(pkg *types.Package) { w.string(w.exportPath(pkg)) } -func (w *exportWriter) qualifiedIdent(obj types.Object) { +func (w *exportWriter) qualifiedType(obj *types.TypeName) { name := w.p.exportName(obj) // Ensure any referenced declarations are written out too. @@ -556,11 +599,11 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { return } w.startType(definedType) - w.qualifiedIdent(t.Obj()) + w.qualifiedType(t.Obj()) case *typeparams.TypeParam: w.startType(typeParamType) - w.qualifiedIdent(t.Obj()) + w.qualifiedType(t.Obj()) case *types.Pointer: w.startType(pointerType) @@ -602,14 +645,17 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { case *types.Struct: w.startType(structType) - w.setPkg(pkg, true) - n := t.NumFields() + if n > 0 { + w.setPkg(t.Field(0).Pkg(), true) // qualifying package for field objects + } else { + w.setPkg(pkg, true) + } w.uint64(uint64(n)) for i := 0; i < n; i++ { f := t.Field(i) w.pos(f.Pos()) - w.string(f.Name()) + w.string(f.Name()) // unexported fields implicitly qualified by prior setPkg w.typ(f.Type(), pkg) w.bool(f.Anonymous()) w.string(t.Tag(i)) // note (or tag) diff --git a/go/internal/gcimporter/iexport_common_test.go b/internal/gcimporter/iexport_common_test.go similarity index 100% rename from go/internal/gcimporter/iexport_common_test.go rename to internal/gcimporter/iexport_common_test.go diff --git a/go/internal/gcimporter/iexport_go118_test.go b/internal/gcimporter/iexport_go118_test.go similarity index 99% rename from go/internal/gcimporter/iexport_go118_test.go rename to internal/gcimporter/iexport_go118_test.go index 5dfa2580f6b..27ba8cec5ac 100644 --- a/go/internal/gcimporter/iexport_go118_test.go +++ b/internal/gcimporter/iexport_go118_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "golang.org/x/tools/go/internal/gcimporter" + "golang.org/x/tools/internal/gcimporter" ) // TODO(rfindley): migrate this to testdata, as has been done in the standard library. diff --git a/go/internal/gcimporter/iexport_test.go b/internal/gcimporter/iexport_test.go similarity index 85% rename from go/internal/gcimporter/iexport_test.go rename to internal/gcimporter/iexport_test.go index f0e83e519fe..93183f9dc6f 100644 --- a/go/internal/gcimporter/iexport_test.go +++ b/internal/gcimporter/iexport_test.go @@ -30,8 +30,9 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/buildutil" - "golang.org/x/tools/go/internal/gcimporter" + "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/loader" + "golang.org/x/tools/internal/gcimporter" "golang.org/x/tools/internal/typeparams/genericfeatures" ) @@ -58,7 +59,8 @@ func readExportFile(filename string) ([]byte, error) { func iexport(fset *token.FileSet, version int, pkg *types.Package) ([]byte, error) { var buf bytes.Buffer - if err := gcimporter.IExportCommon(&buf, fset, false, version, []*types.Package{pkg}); err != nil { + const bundle, shallow = false, false + if err := gcimporter.IExportCommon(&buf, fset, bundle, shallow, version, []*types.Package{pkg}); err != nil { return nil, err } return buf.Bytes(), nil @@ -196,7 +198,7 @@ func testPkg(t *testing.T, fset *token.FileSet, version int, pkg *types.Package, // Compare the packages' corresponding members. for _, name := range pkg.Scope().Names() { - if !ast.IsExported(name) { + if !token.IsExported(name) { continue } obj1 := pkg.Scope().Lookup(name) @@ -403,3 +405,50 @@ func valueToRat(x constant.Value) *big.Rat { } return new(big.Rat).SetInt(new(big.Int).SetBytes(bytes)) } + +// This is a regression test for a bug in iexport of types.Struct: +// unexported fields were losing their implicit package qualifier. +func TestUnexportedStructFields(t *testing.T) { + fset := token.NewFileSet() + export := make(map[string][]byte) + + // process parses and type-checks a single-file + // package and saves its export data. + process := func(path, content string) { + syntax, err := parser.ParseFile(fset, path+"/x.go", content, 0) + if err != nil { + t.Fatal(err) + } + packages := make(map[string]*types.Package) // keys are package paths + cfg := &types.Config{ + Importer: importerFunc(func(path string) (*types.Package, error) { + data, ok := export[path] + if !ok { + return nil, fmt.Errorf("missing export data for %s", path) + } + return gcexportdata.Read(bytes.NewReader(data), fset, packages, path) + }), + } + pkg := types.NewPackage(path, syntax.Name.Name) + check := types.NewChecker(cfg, fset, pkg, nil) + if err := check.Files([]*ast.File{syntax}); err != nil { + t.Fatal(err) + } + var out bytes.Buffer + if err := gcexportdata.Write(&out, fset, pkg); err != nil { + t.Fatal(err) + } + export[path] = out.Bytes() + } + + // Historically this led to a spurious error: + // "cannot convert a.M (variable of type a.MyTime) to type time.Time" + // because the private fields of Time and MyTime were not identical. + process("time", `package time; type Time struct { x, y int }`) + process("a", `package a; import "time"; type MyTime time.Time; var M MyTime`) + process("b", `package b; import ("a"; "time"); var _ = time.Time(a.M)`) +} + +type importerFunc func(path string) (*types.Package, error) + +func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } diff --git a/go/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go similarity index 92% rename from go/internal/gcimporter/iimport.go rename to internal/gcimporter/iimport.go index 28b91b86567..a1c46965350 100644 --- a/go/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -17,6 +17,7 @@ import ( "go/token" "go/types" "io" + "math/big" "sort" "strings" @@ -50,6 +51,8 @@ const ( iexportVersionPosCol = 1 iexportVersionGo1_18 = 2 iexportVersionGenerics = 2 + + iexportVersionCurrent = 2 ) type ident struct { @@ -82,7 +85,7 @@ const ( // If the export data version is not recognized or the format is otherwise // compromised, an error is returned. func IImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (int, *types.Package, error) { - pkgs, err := iimportCommon(fset, imports, data, false, path) + pkgs, err := iimportCommon(fset, imports, data, false, path, nil) if err != nil { return 0, nil, err } @@ -91,11 +94,11 @@ func IImportData(fset *token.FileSet, imports map[string]*types.Package, data [] // IImportBundle imports a set of packages from the serialized package bundle. func IImportBundle(fset *token.FileSet, imports map[string]*types.Package, data []byte) ([]*types.Package, error) { - return iimportCommon(fset, imports, data, true, "") + return iimportCommon(fset, imports, data, true, "", nil) } -func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data []byte, bundle bool, path string) (pkgs []*types.Package, err error) { - const currentVersion = 1 +func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data []byte, bundle bool, path string, insert InsertType) (pkgs []*types.Package, err error) { + const currentVersion = iexportVersionCurrent version := int64(-1) if !debug { defer func() { @@ -144,6 +147,7 @@ func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data p := iimporter{ version: int(version), ipath: path, + insert: insert, stringData: stringData, stringCache: make(map[uint64]string), @@ -184,11 +188,18 @@ func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data } else if pkg.Name() != pkgName { errorf("conflicting names %s and %s for package %q", pkg.Name(), pkgName, path) } + if i == 0 && !bundle { + p.localpkg = pkg + } p.pkgCache[pkgPathOff] = pkg + // Read index for package. nameIndex := make(map[string]uint64) - for nSyms := r.uint64(); nSyms > 0; nSyms-- { + nSyms := r.uint64() + // In shallow mode we don't expect an index for other packages. + assert(nSyms == 0 || p.localpkg == pkg || p.insert == nil) + for ; nSyms > 0; nSyms-- { name := p.stringAt(r.uint64()) nameIndex[name] = r.uint64() } @@ -264,6 +275,9 @@ type iimporter struct { version int ipath string + localpkg *types.Package + insert func(pkg *types.Package, name string) // "shallow" mode only + stringData []byte stringCache map[uint64]string pkgCache map[uint64]*types.Package @@ -307,6 +321,13 @@ func (p *iimporter) doDecl(pkg *types.Package, name string) { off, ok := p.pkgIndex[pkg][name] if !ok { + // In "shallow" mode, call back to the application to + // find the object and insert it into the package scope. + if p.insert != nil { + assert(pkg != p.localpkg) + p.insert(pkg, name) // "can't fail" + return + } errorf("%v.%v not in index", pkg, name) } @@ -512,7 +533,9 @@ func (r *importReader) value() (typ types.Type, val constant.Value) { val = constant.MakeString(r.string()) case types.IsInteger: - val = r.mpint(b) + var x big.Int + r.mpint(&x, b) + val = constant.Make(&x) case types.IsFloat: val = r.mpfloat(b) @@ -561,8 +584,8 @@ func intSize(b *types.Basic) (signed bool, maxBytes uint) { return } -func (r *importReader) mpint(b *types.Basic) constant.Value { - signed, maxBytes := intSize(b) +func (r *importReader) mpint(x *big.Int, typ *types.Basic) { + signed, maxBytes := intSize(typ) maxSmall := 256 - maxBytes if signed { @@ -581,7 +604,8 @@ func (r *importReader) mpint(b *types.Basic) constant.Value { v = ^v } } - return constant.MakeInt64(v) + x.SetInt64(v) + return } v := -n @@ -591,47 +615,23 @@ func (r *importReader) mpint(b *types.Basic) constant.Value { if v < 1 || uint(v) > maxBytes { errorf("weird decoding: %v, %v => %v", n, signed, v) } - - buf := make([]byte, v) - io.ReadFull(&r.declReader, buf) - - // convert to little endian - // TODO(gri) go/constant should have a more direct conversion function - // (e.g., once it supports a big.Float based implementation) - for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 { - buf[i], buf[j] = buf[j], buf[i] - } - - x := constant.MakeFromBytes(buf) + b := make([]byte, v) + io.ReadFull(&r.declReader, b) + x.SetBytes(b) if signed && n&1 != 0 { - x = constant.UnaryOp(token.SUB, x, 0) + x.Neg(x) } - return x } -func (r *importReader) mpfloat(b *types.Basic) constant.Value { - x := r.mpint(b) - if constant.Sign(x) == 0 { - return x - } - - exp := r.int64() - switch { - case exp > 0: - x = constant.Shift(x, token.SHL, uint(exp)) - // Ensure that the imported Kind is Float, else this constant may run into - // bitsize limits on overlarge integers. Eventually we can instead adopt - // the approach of CL 288632, but that CL relies on go/constant APIs that - // were introduced in go1.13. - // - // TODO(rFindley): sync the logic here with tip Go once we no longer - // support go1.12. - x = constant.ToFloat(x) - case exp < 0: - d := constant.Shift(constant.MakeInt64(1), token.SHL, uint(-exp)) - x = constant.BinaryOp(x, token.QUO, d) +func (r *importReader) mpfloat(typ *types.Basic) constant.Value { + var mant big.Int + r.mpint(&mant, typ) + var f big.Float + f.SetInt(&mant) + if f.Sign() != 0 { + f.SetMantExp(&f, int(r.int64())) } - return x + return constant.Make(&f) } func (r *importReader) ident() string { diff --git a/go/internal/gcimporter/israce_test.go b/internal/gcimporter/israce_test.go similarity index 100% rename from go/internal/gcimporter/israce_test.go rename to internal/gcimporter/israce_test.go diff --git a/go/internal/gcimporter/newInterface10.go b/internal/gcimporter/newInterface10.go similarity index 100% rename from go/internal/gcimporter/newInterface10.go rename to internal/gcimporter/newInterface10.go diff --git a/go/internal/gcimporter/newInterface11.go b/internal/gcimporter/newInterface11.go similarity index 100% rename from go/internal/gcimporter/newInterface11.go rename to internal/gcimporter/newInterface11.go diff --git a/internal/gcimporter/shallow_test.go b/internal/gcimporter/shallow_test.go new file mode 100644 index 00000000000..3d8c86a1545 --- /dev/null +++ b/internal/gcimporter/shallow_test.go @@ -0,0 +1,163 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gcimporter_test + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/sync/errgroup" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/gcimporter" + "golang.org/x/tools/internal/testenv" +) + +// TestStd type-checks the standard library using shallow export data. +func TestShallowStd(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode; too slow (https://golang.org/issue/14113)") + } + testenv.NeedsTool(t, "go") + + // Load import graph of the standard library. + // (No parsing or type-checking.) + cfg := &packages.Config{ + Mode: packages.NeedImports | + packages.NeedName | + packages.NeedFiles | // see https://github.com/golang/go/issues/56632 + packages.NeedCompiledGoFiles, + Tests: false, + } + pkgs, err := packages.Load(cfg, "std") + if err != nil { + t.Fatalf("load: %v", err) + } + if len(pkgs) < 200 { + t.Fatalf("too few packages: %d", len(pkgs)) + } + + // Type check the packages in parallel postorder. + done := make(map[*packages.Package]chan struct{}) + packages.Visit(pkgs, nil, func(p *packages.Package) { + done[p] = make(chan struct{}) + }) + packages.Visit(pkgs, nil, + func(pkg *packages.Package) { + go func() { + // Wait for all deps to be done. + for _, imp := range pkg.Imports { + <-done[imp] + } + typecheck(t, pkg) + close(done[pkg]) + }() + }) + for _, root := range pkgs { + <-done[root] + } +} + +// typecheck reads, parses, and type-checks a package. +// It squirrels the export data in the the ppkg.ExportFile field. +func typecheck(t *testing.T, ppkg *packages.Package) { + if ppkg.PkgPath == "unsafe" { + return // unsafe is special + } + + // Create a local FileSet just for this package. + fset := token.NewFileSet() + + // Parse files in parallel. + syntax := make([]*ast.File, len(ppkg.CompiledGoFiles)) + var group errgroup.Group + for i, filename := range ppkg.CompiledGoFiles { + i, filename := i, filename + group.Go(func() error { + f, err := parser.ParseFile(fset, filename, nil, parser.SkipObjectResolution) + if err != nil { + return err // e.g. missing file + } + syntax[i] = f + return nil + }) + } + if err := group.Wait(); err != nil { + t.Fatal(err) + } + // Inv: all files were successfully parsed. + + // Build map of dependencies by package path. + // (We don't compute this mapping for the entire + // packages graph because it is not globally consistent.) + depsByPkgPath := make(map[string]*packages.Package) + { + var visit func(*packages.Package) + visit = func(pkg *packages.Package) { + if depsByPkgPath[pkg.PkgPath] == nil { + depsByPkgPath[pkg.PkgPath] = pkg + for path := range pkg.Imports { + visit(pkg.Imports[path]) + } + } + } + visit(ppkg) + } + + // importer state + var ( + insert func(p *types.Package, name string) + importMap = make(map[string]*types.Package) // keys are PackagePaths + ) + loadFromExportData := func(imp *packages.Package) (*types.Package, error) { + data := []byte(imp.ExportFile) + return gcimporter.IImportShallow(fset, importMap, data, imp.PkgPath, insert) + } + insert = func(p *types.Package, name string) { + imp, ok := depsByPkgPath[p.Path()] + if !ok { + t.Fatalf("can't find dependency: %q", p.Path()) + } + imported, err := loadFromExportData(imp) + if err != nil { + t.Fatalf("unmarshal: %v", err) + } + if imported != p { + t.Fatalf("internal error: inconsistent packages") + } + if obj := imported.Scope().Lookup(name); obj == nil { + t.Fatalf("lookup %q.%s failed", imported.Path(), name) + } + } + + cfg := &types.Config{ + Error: func(e error) { + t.Error(e) + }, + Importer: importerFunc(func(importPath string) (*types.Package, error) { + if importPath == "unsafe" { + return types.Unsafe, nil // unsafe has no exportdata + } + imp, ok := ppkg.Imports[importPath] + if !ok { + return nil, fmt.Errorf("missing import %q", importPath) + } + return loadFromExportData(imp) + }), + } + + // Type-check the syntax trees. + tpkg, _ := cfg.Check(ppkg.PkgPath, fset, syntax, nil) + + // Save the export data. + data, err := gcimporter.IExportShallow(fset, tpkg) + if err != nil { + t.Fatalf("internal error marshalling export data: %v", err) + } + ppkg.ExportFile = string(data) +} diff --git a/internal/gcimporter/stdlib_test.go b/internal/gcimporter/stdlib_test.go new file mode 100644 index 00000000000..ec1be3ea031 --- /dev/null +++ b/internal/gcimporter/stdlib_test.go @@ -0,0 +1,82 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gcimporter_test + +import ( + "bytes" + "fmt" + "go/token" + "go/types" + "testing" + "unsafe" + + "golang.org/x/tools/go/gcexportdata" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/testenv" +) + +// TestStdlib ensures that all packages in std and x/tools can be +// type-checked using export data. Takes around 3s. +func TestStdlib(t *testing.T) { + testenv.NeedsGoPackages(t) + + // gcexportdata.Read rapidly consumes FileSet address space, + // so disable the test on 32-bit machines. + // (We could use a fresh FileSet per type-check, but that + // would require us to re-parse the source using it.) + if unsafe.Sizeof(token.NoPos) < 8 { + t.Skip("skipping test on 32-bit machine") + } + + // Load, parse and type-check the standard library and x/tools. + cfg := &packages.Config{Mode: packages.LoadAllSyntax} + pkgs, err := packages.Load(cfg, "std", "golang.org/x/tools/...") + if err != nil { + t.Fatalf("failed to load/parse/type-check: %v", err) + } + if packages.PrintErrors(pkgs) > 0 { + t.Fatal("there were errors during loading") + } + if len(pkgs) < 240 { + t.Errorf("too few packages (%d) were loaded", len(pkgs)) + } + + export := make(map[string][]byte) // keys are package IDs + + // Re-type check them all in post-order, using export data. + packages.Visit(pkgs, nil, func(pkg *packages.Package) { + packages := make(map[string]*types.Package) // keys are package paths + cfg := &types.Config{ + Error: func(e error) { + t.Errorf("type error: %v", e) + }, + Importer: importerFunc(func(importPath string) (*types.Package, error) { + // Resolve import path to (vendored?) package path. + imported := pkg.Imports[importPath] + + if imported.PkgPath == "unsafe" { + return types.Unsafe, nil // unsafe has no exportdata + } + + data, ok := export[imported.ID] + if !ok { + return nil, fmt.Errorf("missing export data for %s", importPath) + } + return gcexportdata.Read(bytes.NewReader(data), pkg.Fset, packages, imported.PkgPath) + }), + } + + // Re-typecheck the syntax and save the export data in the map. + newPkg := types.NewPackage(pkg.PkgPath, pkg.Name) + check := types.NewChecker(cfg, pkg.Fset, newPkg, nil) + check.Files(pkg.Syntax) + + var out bytes.Buffer + if err := gcexportdata.Write(&out, pkg.Fset, newPkg); err != nil { + t.Fatalf("internal error writing export data: %v", err) + } + export[pkg.ID] = out.Bytes() + }) +} diff --git a/go/internal/gcimporter/support_go117.go b/internal/gcimporter/support_go117.go similarity index 100% rename from go/internal/gcimporter/support_go117.go rename to internal/gcimporter/support_go117.go diff --git a/go/internal/gcimporter/support_go118.go b/internal/gcimporter/support_go118.go similarity index 62% rename from go/internal/gcimporter/support_go118.go rename to internal/gcimporter/support_go118.go index a993843230c..edbe6ea7041 100644 --- a/go/internal/gcimporter/support_go118.go +++ b/internal/gcimporter/support_go118.go @@ -21,3 +21,17 @@ func additionalPredeclared() []types.Type { types.Universe.Lookup("any").Type(), } } + +// See cmd/compile/internal/types.SplitVargenSuffix. +func splitVargenSuffix(name string) (base, suffix string) { + i := len(name) + for i > 0 && name[i-1] >= '0' && name[i-1] <= '9' { + i-- + } + const dot = "·" + if i >= len(dot) && name[i-len(dot):i] == dot { + i -= len(dot) + return name[:i], name[i:] + } + return name, "" +} diff --git a/go/internal/gcimporter/testdata/a.go b/internal/gcimporter/testdata/a.go similarity index 100% rename from go/internal/gcimporter/testdata/a.go rename to internal/gcimporter/testdata/a.go diff --git a/go/internal/gcimporter/testdata/b.go b/internal/gcimporter/testdata/b.go similarity index 100% rename from go/internal/gcimporter/testdata/b.go rename to internal/gcimporter/testdata/b.go diff --git a/go/internal/gcimporter/testdata/exports.go b/internal/gcimporter/testdata/exports.go similarity index 100% rename from go/internal/gcimporter/testdata/exports.go rename to internal/gcimporter/testdata/exports.go diff --git a/go/internal/gcimporter/testdata/issue15920.go b/internal/gcimporter/testdata/issue15920.go similarity index 100% rename from go/internal/gcimporter/testdata/issue15920.go rename to internal/gcimporter/testdata/issue15920.go diff --git a/go/internal/gcimporter/testdata/issue20046.go b/internal/gcimporter/testdata/issue20046.go similarity index 100% rename from go/internal/gcimporter/testdata/issue20046.go rename to internal/gcimporter/testdata/issue20046.go diff --git a/go/internal/gcimporter/testdata/issue25301.go b/internal/gcimporter/testdata/issue25301.go similarity index 100% rename from go/internal/gcimporter/testdata/issue25301.go rename to internal/gcimporter/testdata/issue25301.go diff --git a/go/internal/gcimporter/testdata/issue51836/a.go b/internal/gcimporter/testdata/issue51836/a.go similarity index 100% rename from go/internal/gcimporter/testdata/issue51836/a.go rename to internal/gcimporter/testdata/issue51836/a.go diff --git a/go/internal/gcimporter/testdata/issue51836/aa.go b/internal/gcimporter/testdata/issue51836/aa.go similarity index 100% rename from go/internal/gcimporter/testdata/issue51836/aa.go rename to internal/gcimporter/testdata/issue51836/aa.go diff --git a/go/internal/gcimporter/testdata/p.go b/internal/gcimporter/testdata/p.go similarity index 100% rename from go/internal/gcimporter/testdata/p.go rename to internal/gcimporter/testdata/p.go diff --git a/go/internal/gcimporter/testdata/versions/test.go b/internal/gcimporter/testdata/versions/test.go similarity index 100% rename from go/internal/gcimporter/testdata/versions/test.go rename to internal/gcimporter/testdata/versions/test.go diff --git a/go/internal/gcimporter/testdata/versions/test_go1.11_0i.a b/internal/gcimporter/testdata/versions/test_go1.11_0i.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.11_0i.a rename to internal/gcimporter/testdata/versions/test_go1.11_0i.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.11_6b.a b/internal/gcimporter/testdata/versions/test_go1.11_6b.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.11_6b.a rename to internal/gcimporter/testdata/versions/test_go1.11_6b.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.11_999b.a b/internal/gcimporter/testdata/versions/test_go1.11_999b.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.11_999b.a rename to internal/gcimporter/testdata/versions/test_go1.11_999b.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.11_999i.a b/internal/gcimporter/testdata/versions/test_go1.11_999i.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.11_999i.a rename to internal/gcimporter/testdata/versions/test_go1.11_999i.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.7_0.a b/internal/gcimporter/testdata/versions/test_go1.7_0.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.7_0.a rename to internal/gcimporter/testdata/versions/test_go1.7_0.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.7_1.a b/internal/gcimporter/testdata/versions/test_go1.7_1.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.7_1.a rename to internal/gcimporter/testdata/versions/test_go1.7_1.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.8_4.a b/internal/gcimporter/testdata/versions/test_go1.8_4.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.8_4.a rename to internal/gcimporter/testdata/versions/test_go1.8_4.a diff --git a/go/internal/gcimporter/testdata/versions/test_go1.8_5.a b/internal/gcimporter/testdata/versions/test_go1.8_5.a similarity index 100% rename from go/internal/gcimporter/testdata/versions/test_go1.8_5.a rename to internal/gcimporter/testdata/versions/test_go1.8_5.a diff --git a/internal/gcimporter/unified_no.go b/internal/gcimporter/unified_no.go new file mode 100644 index 00000000000..286bf445483 --- /dev/null +++ b/internal/gcimporter/unified_no.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !(go1.18 && goexperiment.unified) +// +build !go1.18 !goexperiment.unified + +package gcimporter + +const unifiedIR = false diff --git a/internal/gcimporter/unified_yes.go b/internal/gcimporter/unified_yes.go new file mode 100644 index 00000000000..b5d69ffbe68 --- /dev/null +++ b/internal/gcimporter/unified_yes.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 && goexperiment.unified +// +build go1.18,goexperiment.unified + +package gcimporter + +const unifiedIR = true diff --git a/internal/gcimporter/ureader_no.go b/internal/gcimporter/ureader_no.go new file mode 100644 index 00000000000..8eb20729c2a --- /dev/null +++ b/internal/gcimporter/ureader_no.go @@ -0,0 +1,19 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.18 +// +build !go1.18 + +package gcimporter + +import ( + "fmt" + "go/token" + "go/types" +) + +func UImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (_ int, pkg *types.Package, err error) { + err = fmt.Errorf("go/tools compiled with a Go version earlier than 1.18 cannot read unified IR export data") + return +} diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go new file mode 100644 index 00000000000..e09053bd37a --- /dev/null +++ b/internal/gcimporter/ureader_yes.go @@ -0,0 +1,698 @@ +// 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. + +// Derived from go/internal/gcimporter/ureader.go + +//go:build go1.18 +// +build go1.18 + +package gcimporter + +import ( + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/internal/pkgbits" +) + +// A pkgReader holds the shared state for reading a unified IR package +// description. +type pkgReader struct { + pkgbits.PkgDecoder + + fake fakeFileSet + + ctxt *types.Context + imports map[string]*types.Package // previously imported packages, indexed by path + + // lazily initialized arrays corresponding to the unified IR + // PosBase, Pkg, and Type sections, respectively. + posBases []string // position bases (i.e., file names) + pkgs []*types.Package + typs []types.Type + + // laterFns holds functions that need to be invoked at the end of + // import reading. + laterFns []func() + // laterFors is used in case of 'type A B' to ensure that B is processed before A. + laterFors map[types.Type]int + + // ifaces holds a list of constructed Interfaces, which need to have + // Complete called after importing is done. + ifaces []*types.Interface +} + +// later adds a function to be invoked at the end of import reading. +func (pr *pkgReader) later(fn func()) { + pr.laterFns = append(pr.laterFns, fn) +} + +// See cmd/compile/internal/noder.derivedInfo. +type derivedInfo struct { + idx pkgbits.Index + needed bool +} + +// See cmd/compile/internal/noder.typeInfo. +type typeInfo struct { + idx pkgbits.Index + derived bool +} + +func UImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (_ int, pkg *types.Package, err error) { + s := string(data) + s = s[:strings.LastIndex(s, "\n$$\n")] + input := pkgbits.NewPkgDecoder(path, s) + pkg = readUnifiedPackage(fset, nil, imports, input) + return +} + +// laterFor adds a function to be invoked at the end of import reading, and records the type that function is finishing. +func (pr *pkgReader) laterFor(t types.Type, fn func()) { + if pr.laterFors == nil { + pr.laterFors = make(map[types.Type]int) + } + pr.laterFors[t] = len(pr.laterFns) + pr.laterFns = append(pr.laterFns, fn) +} + +// readUnifiedPackage reads a package description from the given +// unified IR export data decoder. +func readUnifiedPackage(fset *token.FileSet, ctxt *types.Context, imports map[string]*types.Package, input pkgbits.PkgDecoder) *types.Package { + pr := pkgReader{ + PkgDecoder: input, + + fake: fakeFileSet{ + fset: fset, + files: make(map[string]*fileInfo), + }, + + ctxt: ctxt, + imports: imports, + + posBases: make([]string, input.NumElems(pkgbits.RelocPosBase)), + pkgs: make([]*types.Package, input.NumElems(pkgbits.RelocPkg)), + typs: make([]types.Type, input.NumElems(pkgbits.RelocType)), + } + defer pr.fake.setLines() + + r := pr.newReader(pkgbits.RelocMeta, pkgbits.PublicRootIdx, pkgbits.SyncPublic) + pkg := r.pkg() + r.Bool() // has init + + for i, n := 0, r.Len(); i < n; i++ { + // As if r.obj(), but avoiding the Scope.Lookup call, + // to avoid eager loading of imports. + r.Sync(pkgbits.SyncObject) + assert(!r.Bool()) + r.p.objIdx(r.Reloc(pkgbits.RelocObj)) + assert(r.Len() == 0) + } + + r.Sync(pkgbits.SyncEOF) + + for _, fn := range pr.laterFns { + fn() + } + + for _, iface := range pr.ifaces { + iface.Complete() + } + + pkg.MarkComplete() + return pkg +} + +// A reader holds the state for reading a single unified IR element +// within a package. +type reader struct { + pkgbits.Decoder + + p *pkgReader + + dict *readerDict +} + +// A readerDict holds the state for type parameters that parameterize +// the current unified IR element. +type readerDict struct { + // bounds is a slice of typeInfos corresponding to the underlying + // bounds of the element's type parameters. + bounds []typeInfo + + // tparams is a slice of the constructed TypeParams for the element. + tparams []*types.TypeParam + + // devived is a slice of types derived from tparams, which may be + // instantiated while reading the current element. + derived []derivedInfo + derivedTypes []types.Type // lazily instantiated from derived +} + +func (pr *pkgReader) newReader(k pkgbits.RelocKind, idx pkgbits.Index, marker pkgbits.SyncMarker) *reader { + return &reader{ + Decoder: pr.NewDecoder(k, idx, marker), + p: pr, + } +} + +// @@@ Positions + +func (r *reader) pos() token.Pos { + r.Sync(pkgbits.SyncPos) + if !r.Bool() { + return token.NoPos + } + + // TODO(mdempsky): Delta encoding. + posBase := r.posBase() + line := r.Uint() + col := r.Uint() + return r.p.fake.pos(posBase, int(line), int(col)) +} + +func (r *reader) posBase() string { + return r.p.posBaseIdx(r.Reloc(pkgbits.RelocPosBase)) +} + +func (pr *pkgReader) posBaseIdx(idx pkgbits.Index) string { + if b := pr.posBases[idx]; b != "" { + return b + } + + r := pr.newReader(pkgbits.RelocPosBase, idx, pkgbits.SyncPosBase) + + // Within types2, position bases have a lot more details (e.g., + // keeping track of where //line directives appeared exactly). + // + // For go/types, we just track the file name. + + filename := r.String() + + if r.Bool() { // file base + // Was: "b = token.NewTrimmedFileBase(filename, true)" + } else { // line base + pos := r.pos() + line := r.Uint() + col := r.Uint() + + // Was: "b = token.NewLineBase(pos, filename, true, line, col)" + _, _, _ = pos, line, col + } + + b := filename + pr.posBases[idx] = b + return b +} + +// @@@ Packages + +func (r *reader) pkg() *types.Package { + r.Sync(pkgbits.SyncPkg) + return r.p.pkgIdx(r.Reloc(pkgbits.RelocPkg)) +} + +func (pr *pkgReader) pkgIdx(idx pkgbits.Index) *types.Package { + // TODO(mdempsky): Consider using some non-nil pointer to indicate + // the universe scope, so we don't need to keep re-reading it. + if pkg := pr.pkgs[idx]; pkg != nil { + return pkg + } + + pkg := pr.newReader(pkgbits.RelocPkg, idx, pkgbits.SyncPkgDef).doPkg() + pr.pkgs[idx] = pkg + return pkg +} + +func (r *reader) doPkg() *types.Package { + path := r.String() + switch path { + case "": + path = r.p.PkgPath() + case "builtin": + return nil // universe + case "unsafe": + return types.Unsafe + } + + if pkg := r.p.imports[path]; pkg != nil { + return pkg + } + + name := r.String() + + pkg := types.NewPackage(path, name) + r.p.imports[path] = pkg + + imports := make([]*types.Package, r.Len()) + for i := range imports { + imports[i] = r.pkg() + } + pkg.SetImports(flattenImports(imports)) + + return pkg +} + +// flattenImports returns the transitive closure of all imported +// packages rooted from pkgs. +func flattenImports(pkgs []*types.Package) []*types.Package { + var res []*types.Package + + seen := make(map[*types.Package]bool) + var add func(pkg *types.Package) + add = func(pkg *types.Package) { + if seen[pkg] { + return + } + seen[pkg] = true + res = append(res, pkg) + for _, imp := range pkg.Imports() { + add(imp) + } + } + + for _, pkg := range pkgs { + add(pkg) + } + return res +} + +// @@@ Types + +func (r *reader) typ() types.Type { + return r.p.typIdx(r.typInfo(), r.dict) +} + +func (r *reader) typInfo() typeInfo { + r.Sync(pkgbits.SyncType) + if r.Bool() { + return typeInfo{idx: pkgbits.Index(r.Len()), derived: true} + } + return typeInfo{idx: r.Reloc(pkgbits.RelocType), derived: false} +} + +func (pr *pkgReader) typIdx(info typeInfo, dict *readerDict) types.Type { + idx := info.idx + var where *types.Type + if info.derived { + where = &dict.derivedTypes[idx] + idx = dict.derived[idx].idx + } else { + where = &pr.typs[idx] + } + + if typ := *where; typ != nil { + return typ + } + + r := pr.newReader(pkgbits.RelocType, idx, pkgbits.SyncTypeIdx) + r.dict = dict + + typ := r.doTyp() + assert(typ != nil) + + // See comment in pkgReader.typIdx explaining how this happens. + if prev := *where; prev != nil { + return prev + } + + *where = typ + return typ +} + +func (r *reader) doTyp() (res types.Type) { + switch tag := pkgbits.CodeType(r.Code(pkgbits.SyncType)); tag { + default: + errorf("unhandled type tag: %v", tag) + panic("unreachable") + + case pkgbits.TypeBasic: + return types.Typ[r.Len()] + + case pkgbits.TypeNamed: + obj, targs := r.obj() + name := obj.(*types.TypeName) + if len(targs) != 0 { + t, _ := types.Instantiate(r.p.ctxt, name.Type(), targs, false) + return t + } + return name.Type() + + case pkgbits.TypeTypeParam: + return r.dict.tparams[r.Len()] + + case pkgbits.TypeArray: + len := int64(r.Uint64()) + return types.NewArray(r.typ(), len) + case pkgbits.TypeChan: + dir := types.ChanDir(r.Len()) + return types.NewChan(dir, r.typ()) + case pkgbits.TypeMap: + return types.NewMap(r.typ(), r.typ()) + case pkgbits.TypePointer: + return types.NewPointer(r.typ()) + case pkgbits.TypeSignature: + return r.signature(nil, nil, nil) + case pkgbits.TypeSlice: + return types.NewSlice(r.typ()) + case pkgbits.TypeStruct: + return r.structType() + case pkgbits.TypeInterface: + return r.interfaceType() + case pkgbits.TypeUnion: + return r.unionType() + } +} + +func (r *reader) structType() *types.Struct { + fields := make([]*types.Var, r.Len()) + var tags []string + for i := range fields { + pos := r.pos() + pkg, name := r.selector() + ftyp := r.typ() + tag := r.String() + embedded := r.Bool() + + fields[i] = types.NewField(pos, pkg, name, ftyp, embedded) + if tag != "" { + for len(tags) < i { + tags = append(tags, "") + } + tags = append(tags, tag) + } + } + return types.NewStruct(fields, tags) +} + +func (r *reader) unionType() *types.Union { + terms := make([]*types.Term, r.Len()) + for i := range terms { + terms[i] = types.NewTerm(r.Bool(), r.typ()) + } + return types.NewUnion(terms) +} + +func (r *reader) interfaceType() *types.Interface { + methods := make([]*types.Func, r.Len()) + embeddeds := make([]types.Type, r.Len()) + implicit := len(methods) == 0 && len(embeddeds) == 1 && r.Bool() + + for i := range methods { + pos := r.pos() + pkg, name := r.selector() + mtyp := r.signature(nil, nil, nil) + methods[i] = types.NewFunc(pos, pkg, name, mtyp) + } + + for i := range embeddeds { + embeddeds[i] = r.typ() + } + + iface := types.NewInterfaceType(methods, embeddeds) + if implicit { + iface.MarkImplicit() + } + + // We need to call iface.Complete(), but if there are any embedded + // defined types, then we may not have set their underlying + // interface type yet. So we need to defer calling Complete until + // after we've called SetUnderlying everywhere. + // + // TODO(mdempsky): After CL 424876 lands, it should be safe to call + // iface.Complete() immediately. + r.p.ifaces = append(r.p.ifaces, iface) + + return iface +} + +func (r *reader) signature(recv *types.Var, rtparams, tparams []*types.TypeParam) *types.Signature { + r.Sync(pkgbits.SyncSignature) + + params := r.params() + results := r.params() + variadic := r.Bool() + + return types.NewSignatureType(recv, rtparams, tparams, params, results, variadic) +} + +func (r *reader) params() *types.Tuple { + r.Sync(pkgbits.SyncParams) + + params := make([]*types.Var, r.Len()) + for i := range params { + params[i] = r.param() + } + + return types.NewTuple(params...) +} + +func (r *reader) param() *types.Var { + r.Sync(pkgbits.SyncParam) + + pos := r.pos() + pkg, name := r.localIdent() + typ := r.typ() + + return types.NewParam(pos, pkg, name, typ) +} + +// @@@ Objects + +func (r *reader) obj() (types.Object, []types.Type) { + r.Sync(pkgbits.SyncObject) + + assert(!r.Bool()) + + pkg, name := r.p.objIdx(r.Reloc(pkgbits.RelocObj)) + obj := pkgScope(pkg).Lookup(name) + + targs := make([]types.Type, r.Len()) + for i := range targs { + targs[i] = r.typ() + } + + return obj, targs +} + +func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { + rname := pr.newReader(pkgbits.RelocName, idx, pkgbits.SyncObject1) + + objPkg, objName := rname.qualifiedIdent() + assert(objName != "") + + tag := pkgbits.CodeObj(rname.Code(pkgbits.SyncCodeObj)) + + if tag == pkgbits.ObjStub { + assert(objPkg == nil || objPkg == types.Unsafe) + return objPkg, objName + } + + // Ignore local types promoted to global scope (#55110). + if _, suffix := splitVargenSuffix(objName); suffix != "" { + return objPkg, objName + } + + if objPkg.Scope().Lookup(objName) == nil { + dict := pr.objDictIdx(idx) + + r := pr.newReader(pkgbits.RelocObj, idx, pkgbits.SyncObject1) + r.dict = dict + + declare := func(obj types.Object) { + objPkg.Scope().Insert(obj) + } + + switch tag { + default: + panic("weird") + + case pkgbits.ObjAlias: + pos := r.pos() + typ := r.typ() + declare(types.NewTypeName(pos, objPkg, objName, typ)) + + case pkgbits.ObjConst: + pos := r.pos() + typ := r.typ() + val := r.Value() + declare(types.NewConst(pos, objPkg, objName, typ, val)) + + case pkgbits.ObjFunc: + pos := r.pos() + tparams := r.typeParamNames() + sig := r.signature(nil, nil, tparams) + declare(types.NewFunc(pos, objPkg, objName, sig)) + + case pkgbits.ObjType: + pos := r.pos() + + obj := types.NewTypeName(pos, objPkg, objName, nil) + named := types.NewNamed(obj, nil, nil) + declare(obj) + + named.SetTypeParams(r.typeParamNames()) + + rhs := r.typ() + pk := r.p + pk.laterFor(named, func() { + // First be sure that the rhs is initialized, if it needs to be initialized. + delete(pk.laterFors, named) // prevent cycles + if i, ok := pk.laterFors[rhs]; ok { + f := pk.laterFns[i] + pk.laterFns[i] = func() {} // function is running now, so replace it with a no-op + f() // initialize RHS + } + underlying := rhs.Underlying() + + // If the underlying type is an interface, we need to + // duplicate its methods so we can replace the receiver + // parameter's type (#49906). + if iface, ok := underlying.(*types.Interface); ok && iface.NumExplicitMethods() != 0 { + methods := make([]*types.Func, iface.NumExplicitMethods()) + for i := range methods { + fn := iface.ExplicitMethod(i) + sig := fn.Type().(*types.Signature) + + recv := types.NewVar(fn.Pos(), fn.Pkg(), "", named) + methods[i] = types.NewFunc(fn.Pos(), fn.Pkg(), fn.Name(), types.NewSignature(recv, sig.Params(), sig.Results(), sig.Variadic())) + } + + embeds := make([]types.Type, iface.NumEmbeddeds()) + for i := range embeds { + embeds[i] = iface.EmbeddedType(i) + } + + newIface := types.NewInterfaceType(methods, embeds) + r.p.ifaces = append(r.p.ifaces, newIface) + underlying = newIface + } + + named.SetUnderlying(underlying) + }) + + for i, n := 0, r.Len(); i < n; i++ { + named.AddMethod(r.method()) + } + + case pkgbits.ObjVar: + pos := r.pos() + typ := r.typ() + declare(types.NewVar(pos, objPkg, objName, typ)) + } + } + + return objPkg, objName +} + +func (pr *pkgReader) objDictIdx(idx pkgbits.Index) *readerDict { + r := pr.newReader(pkgbits.RelocObjDict, idx, pkgbits.SyncObject1) + + var dict readerDict + + if implicits := r.Len(); implicits != 0 { + errorf("unexpected object with %v implicit type parameter(s)", implicits) + } + + dict.bounds = make([]typeInfo, r.Len()) + for i := range dict.bounds { + dict.bounds[i] = r.typInfo() + } + + dict.derived = make([]derivedInfo, r.Len()) + dict.derivedTypes = make([]types.Type, len(dict.derived)) + for i := range dict.derived { + dict.derived[i] = derivedInfo{r.Reloc(pkgbits.RelocType), r.Bool()} + } + + // function references follow, but reader doesn't need those + + return &dict +} + +func (r *reader) typeParamNames() []*types.TypeParam { + r.Sync(pkgbits.SyncTypeParamNames) + + // Note: This code assumes it only processes objects without + // implement type parameters. This is currently fine, because + // reader is only used to read in exported declarations, which are + // always package scoped. + + if len(r.dict.bounds) == 0 { + return nil + } + + // Careful: Type parameter lists may have cycles. To allow for this, + // we construct the type parameter list in two passes: first we + // create all the TypeNames and TypeParams, then we construct and + // set the bound type. + + r.dict.tparams = make([]*types.TypeParam, len(r.dict.bounds)) + for i := range r.dict.bounds { + pos := r.pos() + pkg, name := r.localIdent() + + tname := types.NewTypeName(pos, pkg, name, nil) + r.dict.tparams[i] = types.NewTypeParam(tname, nil) + } + + typs := make([]types.Type, len(r.dict.bounds)) + for i, bound := range r.dict.bounds { + typs[i] = r.p.typIdx(bound, r.dict) + } + + // TODO(mdempsky): This is subtle, elaborate further. + // + // We have to save tparams outside of the closure, because + // typeParamNames() can be called multiple times with the same + // dictionary instance. + // + // Also, this needs to happen later to make sure SetUnderlying has + // been called. + // + // TODO(mdempsky): Is it safe to have a single "later" slice or do + // we need to have multiple passes? See comments on CL 386002 and + // go.dev/issue/52104. + tparams := r.dict.tparams + r.p.later(func() { + for i, typ := range typs { + tparams[i].SetConstraint(typ) + } + }) + + return r.dict.tparams +} + +func (r *reader) method() *types.Func { + r.Sync(pkgbits.SyncMethod) + pos := r.pos() + pkg, name := r.selector() + + rparams := r.typeParamNames() + sig := r.signature(r.param(), rparams, nil) + + _ = r.pos() // TODO(mdempsky): Remove; this is a hacker for linker.go. + return types.NewFunc(pos, pkg, name, sig) +} + +func (r *reader) qualifiedIdent() (*types.Package, string) { return r.ident(pkgbits.SyncSym) } +func (r *reader) localIdent() (*types.Package, string) { return r.ident(pkgbits.SyncLocalIdent) } +func (r *reader) selector() (*types.Package, string) { return r.ident(pkgbits.SyncSelector) } + +func (r *reader) ident(marker pkgbits.SyncMarker) (*types.Package, string) { + r.Sync(marker) + return r.pkg(), r.String() +} + +// pkgScope returns pkg.Scope(). +// If pkg is nil, it returns types.Universe instead. +// +// TODO(mdempsky): Remove after x/tools can depend on Go 1.19. +func pkgScope(pkg *types.Package) *types.Scope { + if pkg != nil { + return pkg.Scope() + } + return types.Universe +} diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go index 67256dc3974..d50551693f3 100644 --- a/internal/gocommand/invoke.go +++ b/internal/gocommand/invoke.go @@ -10,8 +10,10 @@ import ( "context" "fmt" "io" + "log" "os" "regexp" + "runtime" "strconv" "strings" "sync" @@ -232,6 +234,12 @@ func (i *Invocation) run(ctx context.Context, stdout, stderr io.Writer) error { return runCmdContext(ctx, cmd) } +// DebugHangingGoCommands may be set by tests to enable additional +// instrumentation (including panics) for debugging hanging Go commands. +// +// See golang/go#54461 for details. +var DebugHangingGoCommands = false + // runCmdContext is like exec.CommandContext except it sends os.Interrupt // before os.Kill. func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { @@ -243,11 +251,24 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { resChan <- cmd.Wait() }() - select { - case err := <-resChan: - return err - case <-ctx.Done(): + // If we're interested in debugging hanging Go commands, stop waiting after a + // minute and panic with interesting information. + if DebugHangingGoCommands { + select { + case err := <-resChan: + return err + case <-time.After(1 * time.Minute): + HandleHangingGoCommand(cmd.Process) + case <-ctx.Done(): + } + } else { + select { + case err := <-resChan: + return err + case <-ctx.Done(): + } } + // Cancelled. Interrupt and see if it ends voluntarily. cmd.Process.Signal(os.Interrupt) select { @@ -255,11 +276,63 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) error { return err case <-time.After(time.Second): } + // Didn't shut down in response to interrupt. Kill it hard. - cmd.Process.Kill() + // TODO(rfindley): per advice from bcmills@, it may be better to send SIGQUIT + // on certain platforms, such as unix. + if err := cmd.Process.Kill(); err != nil && DebugHangingGoCommands { + // Don't panic here as this reliably fails on windows with EINVAL. + log.Printf("error killing the Go command: %v", err) + } + + // See above: don't wait indefinitely if we're debugging hanging Go commands. + if DebugHangingGoCommands { + select { + case err := <-resChan: + return err + case <-time.After(10 * time.Second): // a shorter wait as resChan should return quickly following Kill + HandleHangingGoCommand(cmd.Process) + } + } return <-resChan } +func HandleHangingGoCommand(proc *os.Process) { + switch runtime.GOOS { + case "linux", "darwin", "freebsd", "netbsd": + fmt.Fprintln(os.Stderr, `DETECTED A HANGING GO COMMAND + +The gopls test runner has detected a hanging go command. In order to debug +this, the output of ps and lsof/fstat is printed below. + +See golang/go#54461 for more details.`) + + fmt.Fprintln(os.Stderr, "\nps axo ppid,pid,command:") + fmt.Fprintln(os.Stderr, "-------------------------") + psCmd := exec.Command("ps", "axo", "ppid,pid,command") + psCmd.Stdout = os.Stderr + psCmd.Stderr = os.Stderr + if err := psCmd.Run(); err != nil { + panic(fmt.Sprintf("running ps: %v", err)) + } + + listFiles := "lsof" + if runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" { + listFiles = "fstat" + } + + fmt.Fprintln(os.Stderr, "\n"+listFiles+":") + fmt.Fprintln(os.Stderr, "-----") + listFilesCmd := exec.Command(listFiles) + listFilesCmd.Stdout = os.Stderr + listFilesCmd.Stderr = os.Stderr + if err := listFilesCmd.Run(); err != nil { + panic(fmt.Sprintf("running %s: %v", listFiles, err)) + } + } + panic(fmt.Sprintf("detected hanging go command (pid %d): see golang/go#54461 for more details", proc.Pid)) +} + func cmdDebugStr(cmd *exec.Cmd) string { env := make(map[string]string) for _, kv := range cmd.Env { diff --git a/internal/gocommand/version.go b/internal/gocommand/version.go index 71304368020..8db5ceb9d51 100644 --- a/internal/gocommand/version.go +++ b/internal/gocommand/version.go @@ -10,8 +10,15 @@ import ( "strings" ) -// GoVersion checks the go version by running "go list" with modules off. -// It returns the X in Go 1.X. +// GoVersion reports the minor version number of the highest release +// tag built into the go command on the PATH. +// +// Note that this may be higher than the version of the go tool used +// to build this application, and thus the versions of the standard +// go/{scanner,parser,ast,types} packages that are linked into it. +// In that case, callers should either downgrade to the version of +// go used to build the application, or report an error that the +// application is too old to use the go command on the PATH. func GoVersion(ctx context.Context, inv Invocation, r *Runner) (int, error) { inv.Verb = "list" inv.Args = []string{"-e", "-f", `{{context.ReleaseTags}}`, `--`, `unsafe`} @@ -38,7 +45,7 @@ func GoVersion(ctx context.Context, inv Invocation, r *Runner) (int, error) { if len(stdout) < 3 { return 0, fmt.Errorf("bad ReleaseTags output: %q", stdout) } - // Split up "[go1.1 go1.15]" + // Split up "[go1.1 go1.15]" and return highest go1.X value. tags := strings.Fields(stdout[1 : len(stdout)-2]) for i := len(tags) - 1; i >= 0; i-- { var version int diff --git a/internal/goroot/importcfg.go b/internal/goroot/importcfg.go new file mode 100644 index 00000000000..6575cfb9df6 --- /dev/null +++ b/internal/goroot/importcfg.go @@ -0,0 +1,71 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package goroot is a copy of package internal/goroot +// in the main GO repot. It provides a utility to produce +// an importcfg and import path to package file map mapping +// standard library packages to the locations of their export +// data files. +package goroot + +import ( + "bytes" + "fmt" + "os/exec" + "strings" + "sync" +) + +// Importcfg returns an importcfg file to be passed to the +// Go compiler that contains the cached paths for the .a files for the +// standard library. +func Importcfg() (string, error) { + var icfg bytes.Buffer + + m, err := PkgfileMap() + if err != nil { + return "", err + } + fmt.Fprintf(&icfg, "# import config") + for importPath, export := range m { + if importPath != "unsafe" && export != "" { // unsafe + fmt.Fprintf(&icfg, "\npackagefile %s=%s", importPath, export) + } + } + s := icfg.String() + return s, nil +} + +var ( + stdlibPkgfileMap map[string]string + stdlibPkgfileErr error + once sync.Once +) + +// PkgfileMap returns a map of package paths to the location on disk +// of the .a file for the package. +// The caller must not modify the map. +func PkgfileMap() (map[string]string, error) { + once.Do(func() { + m := make(map[string]string) + output, err := exec.Command("go", "list", "-export", "-e", "-f", "{{.ImportPath}} {{.Export}}", "std", "cmd").Output() + if err != nil { + stdlibPkgfileErr = err + } + for _, line := range strings.Split(string(output), "\n") { + if line == "" { + continue + } + sp := strings.SplitN(line, " ", 2) + if len(sp) != 2 { + err = fmt.Errorf("determining pkgfile map: invalid line in go list output: %q", line) + return + } + importPath, export := sp[0], sp[1] + m[importPath] = export + } + stdlibPkgfileMap = m + }) + return stdlibPkgfileMap, stdlibPkgfileErr +} diff --git a/internal/imports/fix.go b/internal/imports/fix.go index d859617b774..9b7b106fde1 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -796,7 +796,7 @@ func GetPackageExports(ctx context.Context, wrapped func(PackageExport), searchP return getCandidatePkgs(ctx, callback, filename, filePkg, env) } -var RequiredGoEnvVars = []string{"GO111MODULE", "GOFLAGS", "GOINSECURE", "GOMOD", "GOMODCACHE", "GONOPROXY", "GONOSUMDB", "GOPATH", "GOPROXY", "GOROOT", "GOSUMDB"} +var RequiredGoEnvVars = []string{"GO111MODULE", "GOFLAGS", "GOINSECURE", "GOMOD", "GOMODCACHE", "GONOPROXY", "GONOSUMDB", "GOPATH", "GOPROXY", "GOROOT", "GOSUMDB", "GOWORK"} // ProcessEnv contains environment variables and settings that affect the use of // the go command, the go/build package, etc. @@ -807,6 +807,11 @@ type ProcessEnv struct { ModFlag string ModFile string + // SkipPathInScan returns true if the path should be skipped from scans of + // the RootCurrentModule root type. The function argument is a clean, + // absolute path. + SkipPathInScan func(string) bool + // Env overrides the OS environment, and can be used to specify // GOPROXY, GO111MODULE, etc. PATH cannot be set here, because // exec.Command will not honor it. @@ -906,7 +911,7 @@ func (e *ProcessEnv) GetResolver() (Resolver, error) { if err := e.init(); err != nil { return nil, err } - if len(e.Env["GOMOD"]) == 0 { + if len(e.Env["GOMOD"]) == 0 && len(e.Env["GOWORK"]) == 0 { e.resolver = newGopathResolver(e) return e.resolver, nil } @@ -1367,9 +1372,9 @@ func (r *gopathResolver) scan(ctx context.Context, callback *scanCallback) error return err } var roots []gopathwalk.Root - roots = append(roots, gopathwalk.Root{filepath.Join(goenv["GOROOT"], "src"), gopathwalk.RootGOROOT}) + roots = append(roots, gopathwalk.Root{Path: filepath.Join(goenv["GOROOT"], "src"), Type: gopathwalk.RootGOROOT}) for _, p := range filepath.SplitList(goenv["GOPATH"]) { - roots = append(roots, gopathwalk.Root{filepath.Join(p, "src"), gopathwalk.RootGOPATH}) + roots = append(roots, gopathwalk.Root{Path: filepath.Join(p, "src"), Type: gopathwalk.RootGOPATH}) } // The callback is not necessarily safe to use in the goroutine below. Process roots eagerly. roots = filterRoots(roots, callback.rootFound) diff --git a/internal/imports/mkstdlib.go b/internal/imports/mkstdlib.go index 47714bf0719..a73f024c1d0 100644 --- a/internal/imports/mkstdlib.go +++ b/internal/imports/mkstdlib.go @@ -23,6 +23,7 @@ import ( "regexp" "runtime" "sort" + "strings" exec "golang.org/x/sys/execabs" ) @@ -48,34 +49,15 @@ func main() { outf := func(format string, args ...interface{}) { fmt.Fprintf(&buf, format, args...) } + outf(`// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +`) outf("// Code generated by mkstdlib.go. DO NOT EDIT.\n\n") outf("package imports\n") outf("var stdlib = map[string][]string{\n") - f := io.MultiReader( - mustOpen(api("go1.txt")), - mustOpen(api("go1.1.txt")), - mustOpen(api("go1.2.txt")), - mustOpen(api("go1.3.txt")), - mustOpen(api("go1.4.txt")), - mustOpen(api("go1.5.txt")), - mustOpen(api("go1.6.txt")), - mustOpen(api("go1.7.txt")), - mustOpen(api("go1.8.txt")), - mustOpen(api("go1.9.txt")), - mustOpen(api("go1.10.txt")), - mustOpen(api("go1.11.txt")), - mustOpen(api("go1.12.txt")), - mustOpen(api("go1.13.txt")), - mustOpen(api("go1.14.txt")), - mustOpen(api("go1.15.txt")), - mustOpen(api("go1.16.txt")), - mustOpen(api("go1.17.txt")), - mustOpen(api("go1.18.txt")), - - // The API of the syscall/js package needs to be computed explicitly, - // because it's not included in the GOROOT/api/go1.*.txt files at this time. - syscallJSAPI(), - ) + f := readAPI() sc := bufio.NewScanner(f) pkgs := map[string]map[string]bool{ @@ -100,7 +82,7 @@ func main() { } sort.Strings(paths) for _, path := range paths { - outf("\t%q: []string{\n", path) + outf("\t%q: {\n", path) pkg := pkgs[path] var syms []string for sym := range pkg { @@ -123,6 +105,25 @@ func main() { } } +// readAPI opens an io.Reader that reads all stdlib API content. +func readAPI() io.Reader { + entries, err := os.ReadDir(filepath.Join(runtime.GOROOT(), "api")) + if err != nil { + log.Fatal(err) + } + var readers []io.Reader + for _, entry := range entries { + name := entry.Name() + if strings.HasPrefix(name, "go") && strings.HasSuffix(name, ".txt") { + readers = append(readers, mustOpen(api(name))) + } + } + // The API of the syscall/js package needs to be computed explicitly, + // because it's not included in the GOROOT/api/go1.*.txt files at this time. + readers = append(readers, syscallJSAPI()) + return io.MultiReader(readers...) +} + // syscallJSAPI returns the API of the syscall/js package. // It's computed from the contents of $(go env GOROOT)/src/syscall/js. func syscallJSAPI() io.Reader { diff --git a/internal/imports/mod.go b/internal/imports/mod.go index 2bcf41f5fa7..7d99d04ca8a 100644 --- a/internal/imports/mod.go +++ b/internal/imports/mod.go @@ -70,9 +70,17 @@ func (r *ModuleResolver) init() error { Logf: r.env.Logf, WorkingDir: r.env.WorkingDir, } - vendorEnabled, mainModVendor, err := gocommand.VendorEnabled(context.TODO(), inv, r.env.GocmdRunner) - if err != nil { - return err + + vendorEnabled := false + var mainModVendor *gocommand.ModuleJSON + + // Module vendor directories are ignored in workspace mode: + // https://go.googlesource.com/proposal/+/master/design/45713-workspace.md + if len(r.env.Env["GOWORK"]) == 0 { + vendorEnabled, mainModVendor, err = gocommand.VendorEnabled(context.TODO(), inv, r.env.GocmdRunner) + if err != nil { + return err + } } if mainModVendor != nil && vendorEnabled { @@ -121,22 +129,22 @@ func (r *ModuleResolver) init() error { }) r.roots = []gopathwalk.Root{ - {filepath.Join(goenv["GOROOT"], "/src"), gopathwalk.RootGOROOT}, + {Path: filepath.Join(goenv["GOROOT"], "/src"), Type: gopathwalk.RootGOROOT}, } r.mainByDir = make(map[string]*gocommand.ModuleJSON) for _, main := range r.mains { - r.roots = append(r.roots, gopathwalk.Root{main.Dir, gopathwalk.RootCurrentModule}) + r.roots = append(r.roots, gopathwalk.Root{Path: main.Dir, Type: gopathwalk.RootCurrentModule}) r.mainByDir[main.Dir] = main } if vendorEnabled { - r.roots = append(r.roots, gopathwalk.Root{r.dummyVendorMod.Dir, gopathwalk.RootOther}) + r.roots = append(r.roots, gopathwalk.Root{Path: r.dummyVendorMod.Dir, Type: gopathwalk.RootOther}) } else { addDep := func(mod *gocommand.ModuleJSON) { if mod.Replace == nil { // This is redundant with the cache, but we'll skip it cheaply enough. - r.roots = append(r.roots, gopathwalk.Root{mod.Dir, gopathwalk.RootModuleCache}) + r.roots = append(r.roots, gopathwalk.Root{Path: mod.Dir, Type: gopathwalk.RootModuleCache}) } else { - r.roots = append(r.roots, gopathwalk.Root{mod.Dir, gopathwalk.RootOther}) + r.roots = append(r.roots, gopathwalk.Root{Path: mod.Dir, Type: gopathwalk.RootOther}) } } // Walk dependent modules before scanning the full mod cache, direct deps first. @@ -150,7 +158,7 @@ func (r *ModuleResolver) init() error { addDep(mod) } } - r.roots = append(r.roots, gopathwalk.Root{r.moduleCacheDir, gopathwalk.RootModuleCache}) + r.roots = append(r.roots, gopathwalk.Root{Path: r.moduleCacheDir, Type: gopathwalk.RootModuleCache}) } r.scannedRoots = map[gopathwalk.Root]bool{} @@ -458,6 +466,16 @@ func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error // We assume cached directories are fully cached, including all their // children, and have not changed. We can skip them. skip := func(root gopathwalk.Root, dir string) bool { + if r.env.SkipPathInScan != nil && root.Type == gopathwalk.RootCurrentModule { + if root.Path == dir { + return false + } + + if r.env.SkipPathInScan(filepath.Clean(dir)) { + return true + } + } + info, ok := r.cacheLoad(dir) if !ok { return false diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go index 5f71805fa77..8063dbe0f74 100644 --- a/internal/imports/mod_test.go +++ b/internal/imports/mod_test.go @@ -29,7 +29,7 @@ import ( // Tests that we can find packages in the stdlib. func TestScanStdlib(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x `, "") @@ -42,7 +42,7 @@ module x // where the module is in scope -- here we have to figure out the import path // without any help from go list. func TestScanOutOfScopeNestedModule(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -68,7 +68,7 @@ package x`, "") // Tests that we don't find a nested module contained in a local replace target. // The code for this case is too annoying to write, so it's just ignored. func TestScanNestedModuleInLocalReplace(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -107,7 +107,7 @@ package z // Tests that path encoding is handled correctly. Adapted from mod_case.txt. func TestModCase(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -124,7 +124,7 @@ import _ "rsc.io/QUOTE/QUOTE" // Not obviously relevant to goimports. Adapted from mod_domain_root.txt anyway. func TestModDomainRoot(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -140,7 +140,7 @@ import _ "example.com" // Tests that scanning the module cache > 1 time is able to find the same module. func TestModMultipleScans(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -159,7 +159,7 @@ import _ "example.com" // Tests that scanning the module cache > 1 time is able to find the same module // in the module cache. func TestModMultipleScansWithSubdirs(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -178,7 +178,7 @@ import _ "rsc.io/quote" // Tests that scanning the module cache > 1 after changing a package in module cache to make it unimportable // is able to find the same module. func TestModCacheEditModFile(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -219,7 +219,7 @@ import _ "rsc.io/quote" // Tests that -mod=vendor works. Adapted from mod_vendor_build.txt. func TestModVendorBuild(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module m go 1.12 @@ -250,7 +250,7 @@ import _ "rsc.io/sampler" // Tests that -mod=vendor is auto-enabled only for go1.14 and higher. // Vaguely inspired by mod_vendor_auto.txt. func TestModVendorAuto(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module m go 1.14 @@ -276,7 +276,7 @@ import _ "rsc.io/sampler" // Tests that a module replace works. Adapted from mod_list.txt. We start with // go.mod2; the first part of the test is irrelevant. func TestModList(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x require rsc.io/quote v1.5.1 @@ -293,7 +293,7 @@ import _ "rsc.io/quote" // Tests that a local replace works. Adapted from mod_local_replace.txt. func TestModLocalReplace(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- x/y/go.mod -- module x/y require zz v1.0.0 @@ -317,7 +317,7 @@ package z // Tests that the package at the root of the main module can be found. // Adapted from the first part of mod_multirepo.txt. func TestModMultirepo1(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module rsc.io/quote @@ -333,7 +333,7 @@ package quote // of mod_multirepo.txt (We skip the case where it doesn't have a go.mod // entry -- we just don't work in that case.) func TestModMultirepo3(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module rsc.io/quote @@ -352,7 +352,7 @@ import _ "rsc.io/quote/v2" // Tests that a nested module is found in the module cache, even though // it's checked out. Adapted from the fourth part of mod_multirepo.txt. func TestModMultirepo4(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module rsc.io/quote require rsc.io/quote/v2 v2.0.1 @@ -376,7 +376,7 @@ import _ "rsc.io/quote/v2" // Tests a simple module dependency. Adapted from the first part of mod_replace.txt. func TestModReplace1(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module quoter @@ -392,7 +392,7 @@ package main // Tests a local replace. Adapted from the second part of mod_replace.txt. func TestModReplace2(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module quoter @@ -418,7 +418,7 @@ import "rsc.io/sampler" // Tests that a module can be replaced by a different module path. Adapted // from the third part of mod_replace.txt. func TestModReplace3(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module quoter @@ -451,7 +451,7 @@ package quote // mod_replace_import.txt, with example.com/v changed to /vv because Go 1.11 // thinks /v is an invalid major version. func TestModReplaceImport(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module example.com/m @@ -556,7 +556,7 @@ package v func TestModWorkspace(t *testing.T) { testenv.NeedsGo1Point(t, 18) - mt := setup(t, ` + mt := setup(t, nil, ` -- go.work -- go 1.18 @@ -592,7 +592,7 @@ package b func TestModWorkspaceReplace(t *testing.T) { testenv.NeedsGo1Point(t, 18) - mt := setup(t, ` + mt := setup(t, nil, ` -- go.work -- use m @@ -651,7 +651,7 @@ func G() { func TestModWorkspaceReplaceOverride(t *testing.T) { testenv.NeedsGo1Point(t, 18) - mt := setup(t, `-- go.work -- + mt := setup(t, nil, `-- go.work -- use m use n replace example.com/dep => ./dep3 @@ -716,7 +716,7 @@ func G() { func TestModWorkspacePrune(t *testing.T) { testenv.NeedsGo1Point(t, 18) - mt := setup(t, ` + mt := setup(t, nil, ` -- go.work -- go 1.18 @@ -885,7 +885,7 @@ package z // Tests that we handle GO111MODULE=on with no go.mod file. See #30855. func TestNoMainModule(t *testing.T) { testenv.NeedsGo1Point(t, 12) - mt := setup(t, ` + mt := setup(t, map[string]string{"GO111MODULE": "on"}, ` -- x.go -- package x `, "") @@ -993,7 +993,9 @@ type modTest struct { // setup builds a test environment from a txtar and supporting modules // in testdata/mod, along the lines of TestScript in cmd/go. -func setup(t *testing.T, main, wd string) *modTest { +// +// extraEnv is applied on top of the default test env. +func setup(t *testing.T, extraEnv map[string]string, main, wd string) *modTest { t.Helper() testenv.NeedsGo1Point(t, 11) testenv.NeedsTool(t, "go") @@ -1023,13 +1025,16 @@ func setup(t *testing.T, main, wd string) *modTest { Env: map[string]string{ "GOPATH": filepath.Join(dir, "gopath"), "GOMODCACHE": "", - "GO111MODULE": "on", + "GO111MODULE": "auto", "GOSUMDB": "off", "GOPROXY": proxydir.ToURL(proxyDir), }, WorkingDir: filepath.Join(mainDir, wd), GocmdRunner: &gocommand.Runner{}, } + for k, v := range extraEnv { + env.Env[k] = v + } if *testDebug { env.Logf = log.Printf } @@ -1168,7 +1173,7 @@ func removeDir(dir string) { // Tests that findModFile can find the mod files from a path in the module cache. func TestFindModFileModCache(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module x @@ -1220,7 +1225,7 @@ func TestInvalidModCache(t *testing.T) { } func TestGetCandidatesRanking(t *testing.T) { - mt := setup(t, ` + mt := setup(t, nil, ` -- go.mod -- module example.com diff --git a/internal/imports/zstdlib.go b/internal/imports/zstdlib.go index 437fbb78dbd..5db9b2d4c73 100644 --- a/internal/imports/zstdlib.go +++ b/internal/imports/zstdlib.go @@ -1,9 +1,13 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + // Code generated by mkstdlib.go. DO NOT EDIT. package imports var stdlib = map[string][]string{ - "archive/tar": []string{ + "archive/tar": { "ErrFieldTooLong", "ErrHeader", "ErrWriteAfterClose", @@ -34,7 +38,7 @@ var stdlib = map[string][]string{ "TypeXHeader", "Writer", }, - "archive/zip": []string{ + "archive/zip": { "Compressor", "Decompressor", "Deflate", @@ -54,7 +58,7 @@ var stdlib = map[string][]string{ "Store", "Writer", }, - "bufio": []string{ + "bufio": { "ErrAdvanceTooFar", "ErrBadReadCount", "ErrBufferFull", @@ -81,7 +85,7 @@ var stdlib = map[string][]string{ "SplitFunc", "Writer", }, - "bytes": []string{ + "bytes": { "Buffer", "Compare", "Contains", @@ -138,11 +142,11 @@ var stdlib = map[string][]string{ "TrimSpace", "TrimSuffix", }, - "compress/bzip2": []string{ + "compress/bzip2": { "NewReader", "StructuralError", }, - "compress/flate": []string{ + "compress/flate": { "BestCompression", "BestSpeed", "CorruptInputError", @@ -160,7 +164,7 @@ var stdlib = map[string][]string{ "WriteError", "Writer", }, - "compress/gzip": []string{ + "compress/gzip": { "BestCompression", "BestSpeed", "DefaultCompression", @@ -175,7 +179,7 @@ var stdlib = map[string][]string{ "Reader", "Writer", }, - "compress/lzw": []string{ + "compress/lzw": { "LSB", "MSB", "NewReader", @@ -184,7 +188,7 @@ var stdlib = map[string][]string{ "Reader", "Writer", }, - "compress/zlib": []string{ + "compress/zlib": { "BestCompression", "BestSpeed", "DefaultCompression", @@ -201,7 +205,7 @@ var stdlib = map[string][]string{ "Resetter", "Writer", }, - "container/heap": []string{ + "container/heap": { "Fix", "Init", "Interface", @@ -209,16 +213,16 @@ var stdlib = map[string][]string{ "Push", "Remove", }, - "container/list": []string{ + "container/list": { "Element", "List", "New", }, - "container/ring": []string{ + "container/ring": { "New", "Ring", }, - "context": []string{ + "context": { "Background", "CancelFunc", "Canceled", @@ -230,7 +234,7 @@ var stdlib = map[string][]string{ "WithTimeout", "WithValue", }, - "crypto": []string{ + "crypto": { "BLAKE2b_256", "BLAKE2b_384", "BLAKE2b_512", @@ -259,12 +263,12 @@ var stdlib = map[string][]string{ "Signer", "SignerOpts", }, - "crypto/aes": []string{ + "crypto/aes": { "BlockSize", "KeySizeError", "NewCipher", }, - "crypto/cipher": []string{ + "crypto/cipher": { "AEAD", "Block", "BlockMode", @@ -281,13 +285,13 @@ var stdlib = map[string][]string{ "StreamReader", "StreamWriter", }, - "crypto/des": []string{ + "crypto/des": { "BlockSize", "KeySizeError", "NewCipher", "NewTripleDESCipher", }, - "crypto/dsa": []string{ + "crypto/dsa": { "ErrInvalidPublicKey", "GenerateKey", "GenerateParameters", @@ -302,7 +306,7 @@ var stdlib = map[string][]string{ "Sign", "Verify", }, - "crypto/ecdsa": []string{ + "crypto/ecdsa": { "GenerateKey", "PrivateKey", "PublicKey", @@ -311,7 +315,7 @@ var stdlib = map[string][]string{ "Verify", "VerifyASN1", }, - "crypto/ed25519": []string{ + "crypto/ed25519": { "GenerateKey", "NewKeyFromSeed", "PrivateKey", @@ -323,7 +327,7 @@ var stdlib = map[string][]string{ "SignatureSize", "Verify", }, - "crypto/elliptic": []string{ + "crypto/elliptic": { "Curve", "CurveParams", "GenerateKey", @@ -336,28 +340,28 @@ var stdlib = map[string][]string{ "Unmarshal", "UnmarshalCompressed", }, - "crypto/hmac": []string{ + "crypto/hmac": { "Equal", "New", }, - "crypto/md5": []string{ + "crypto/md5": { "BlockSize", "New", "Size", "Sum", }, - "crypto/rand": []string{ + "crypto/rand": { "Int", "Prime", "Read", "Reader", }, - "crypto/rc4": []string{ + "crypto/rc4": { "Cipher", "KeySizeError", "NewCipher", }, - "crypto/rsa": []string{ + "crypto/rsa": { "CRTValue", "DecryptOAEP", "DecryptPKCS1v15", @@ -382,13 +386,13 @@ var stdlib = map[string][]string{ "VerifyPKCS1v15", "VerifyPSS", }, - "crypto/sha1": []string{ + "crypto/sha1": { "BlockSize", "New", "Size", "Sum", }, - "crypto/sha256": []string{ + "crypto/sha256": { "BlockSize", "New", "New224", @@ -397,7 +401,7 @@ var stdlib = map[string][]string{ "Sum224", "Sum256", }, - "crypto/sha512": []string{ + "crypto/sha512": { "BlockSize", "New", "New384", @@ -412,7 +416,7 @@ var stdlib = map[string][]string{ "Sum512_224", "Sum512_256", }, - "crypto/subtle": []string{ + "crypto/subtle": { "ConstantTimeByteEq", "ConstantTimeCompare", "ConstantTimeCopy", @@ -420,7 +424,7 @@ var stdlib = map[string][]string{ "ConstantTimeLessOrEq", "ConstantTimeSelect", }, - "crypto/tls": []string{ + "crypto/tls": { "Certificate", "CertificateRequestInfo", "CipherSuite", @@ -506,7 +510,7 @@ var stdlib = map[string][]string{ "X25519", "X509KeyPair", }, - "crypto/x509": []string{ + "crypto/x509": { "CANotAuthorizedForExtKeyUsage", "CANotAuthorizedForThisName", "CertPool", @@ -588,6 +592,7 @@ var stdlib = map[string][]string{ "ParsePKCS1PublicKey", "ParsePKCS8PrivateKey", "ParsePKIXPublicKey", + "ParseRevocationList", "PublicKeyAlgorithm", "PureEd25519", "RSA", @@ -611,7 +616,7 @@ var stdlib = map[string][]string{ "UnknownSignatureAlgorithm", "VerifyOptions", }, - "crypto/x509/pkix": []string{ + "crypto/x509/pkix": { "AlgorithmIdentifier", "AttributeTypeAndValue", "AttributeTypeAndValueSET", @@ -623,7 +628,7 @@ var stdlib = map[string][]string{ "RevokedCertificate", "TBSCertificateList", }, - "database/sql": []string{ + "database/sql": { "ColumnType", "Conn", "DB", @@ -664,7 +669,7 @@ var stdlib = map[string][]string{ "Tx", "TxOptions", }, - "database/sql/driver": []string{ + "database/sql/driver": { "Bool", "ColumnConverter", "Conn", @@ -712,12 +717,12 @@ var stdlib = map[string][]string{ "ValueConverter", "Valuer", }, - "debug/buildinfo": []string{ + "debug/buildinfo": { "BuildInfo", "Read", "ReadFile", }, - "debug/dwarf": []string{ + "debug/dwarf": { "AddrType", "ArrayType", "Attr", @@ -968,7 +973,7 @@ var stdlib = map[string][]string{ "UnsupportedType", "VoidType", }, - "debug/elf": []string{ + "debug/elf": { "ARM_MAGIC_TRAMP_NUMBER", "COMPRESS_HIOS", "COMPRESS_HIPROC", @@ -1238,6 +1243,7 @@ var stdlib = map[string][]string{ "EM_L10M", "EM_LANAI", "EM_LATTICEMICO32", + "EM_LOONGARCH", "EM_M16C", "EM_M32", "EM_M32C", @@ -1820,6 +1826,57 @@ var stdlib = map[string][]string{ "R_ARM_XPC25", "R_INFO", "R_INFO32", + "R_LARCH", + "R_LARCH_32", + "R_LARCH_64", + "R_LARCH_ADD16", + "R_LARCH_ADD24", + "R_LARCH_ADD32", + "R_LARCH_ADD64", + "R_LARCH_ADD8", + "R_LARCH_COPY", + "R_LARCH_IRELATIVE", + "R_LARCH_JUMP_SLOT", + "R_LARCH_MARK_LA", + "R_LARCH_MARK_PCREL", + "R_LARCH_NONE", + "R_LARCH_RELATIVE", + "R_LARCH_SOP_ADD", + "R_LARCH_SOP_AND", + "R_LARCH_SOP_ASSERT", + "R_LARCH_SOP_IF_ELSE", + "R_LARCH_SOP_NOT", + "R_LARCH_SOP_POP_32_S_0_10_10_16_S2", + "R_LARCH_SOP_POP_32_S_0_5_10_16_S2", + "R_LARCH_SOP_POP_32_S_10_12", + "R_LARCH_SOP_POP_32_S_10_16", + "R_LARCH_SOP_POP_32_S_10_16_S2", + "R_LARCH_SOP_POP_32_S_10_5", + "R_LARCH_SOP_POP_32_S_5_20", + "R_LARCH_SOP_POP_32_U", + "R_LARCH_SOP_POP_32_U_10_12", + "R_LARCH_SOP_PUSH_ABSOLUTE", + "R_LARCH_SOP_PUSH_DUP", + "R_LARCH_SOP_PUSH_GPREL", + "R_LARCH_SOP_PUSH_PCREL", + "R_LARCH_SOP_PUSH_PLT_PCREL", + "R_LARCH_SOP_PUSH_TLS_GD", + "R_LARCH_SOP_PUSH_TLS_GOT", + "R_LARCH_SOP_PUSH_TLS_TPREL", + "R_LARCH_SOP_SL", + "R_LARCH_SOP_SR", + "R_LARCH_SOP_SUB", + "R_LARCH_SUB16", + "R_LARCH_SUB24", + "R_LARCH_SUB32", + "R_LARCH_SUB64", + "R_LARCH_SUB8", + "R_LARCH_TLS_DTPMOD32", + "R_LARCH_TLS_DTPMOD64", + "R_LARCH_TLS_DTPREL32", + "R_LARCH_TLS_DTPREL64", + "R_LARCH_TLS_TPREL32", + "R_LARCH_TLS_TPREL64", "R_MIPS", "R_MIPS_16", "R_MIPS_26", @@ -2315,7 +2372,7 @@ var stdlib = map[string][]string{ "Type", "Version", }, - "debug/gosym": []string{ + "debug/gosym": { "DecodingError", "Func", "LineTable", @@ -2327,7 +2384,7 @@ var stdlib = map[string][]string{ "UnknownFileError", "UnknownLineError", }, - "debug/macho": []string{ + "debug/macho": { "ARM64_RELOC_ADDEND", "ARM64_RELOC_BRANCH26", "ARM64_RELOC_GOT_LOAD_PAGE21", @@ -2457,13 +2514,20 @@ var stdlib = map[string][]string{ "X86_64_RELOC_TLV", "X86_64_RELOC_UNSIGNED", }, - "debug/pe": []string{ + "debug/pe": { "COFFSymbol", + "COFFSymbolAuxFormat5", "COFFSymbolSize", "DataDirectory", "File", "FileHeader", "FormatError", + "IMAGE_COMDAT_SELECT_ANY", + "IMAGE_COMDAT_SELECT_ASSOCIATIVE", + "IMAGE_COMDAT_SELECT_EXACT_MATCH", + "IMAGE_COMDAT_SELECT_LARGEST", + "IMAGE_COMDAT_SELECT_NODUPLICATES", + "IMAGE_COMDAT_SELECT_SAME_SIZE", "IMAGE_DIRECTORY_ENTRY_ARCHITECTURE", "IMAGE_DIRECTORY_ENTRY_BASERELOC", "IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT", @@ -2508,6 +2572,8 @@ var stdlib = map[string][]string{ "IMAGE_FILE_MACHINE_EBC", "IMAGE_FILE_MACHINE_I386", "IMAGE_FILE_MACHINE_IA64", + "IMAGE_FILE_MACHINE_LOONGARCH32", + "IMAGE_FILE_MACHINE_LOONGARCH64", "IMAGE_FILE_MACHINE_M32R", "IMAGE_FILE_MACHINE_MIPS16", "IMAGE_FILE_MACHINE_MIPSFPU", @@ -2527,6 +2593,14 @@ var stdlib = map[string][]string{ "IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP", "IMAGE_FILE_SYSTEM", "IMAGE_FILE_UP_SYSTEM_ONLY", + "IMAGE_SCN_CNT_CODE", + "IMAGE_SCN_CNT_INITIALIZED_DATA", + "IMAGE_SCN_CNT_UNINITIALIZED_DATA", + "IMAGE_SCN_LNK_COMDAT", + "IMAGE_SCN_MEM_DISCARDABLE", + "IMAGE_SCN_MEM_EXECUTE", + "IMAGE_SCN_MEM_READ", + "IMAGE_SCN_MEM_WRITE", "IMAGE_SUBSYSTEM_EFI_APPLICATION", "IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER", "IMAGE_SUBSYSTEM_EFI_ROM", @@ -2553,7 +2627,7 @@ var stdlib = map[string][]string{ "StringTable", "Symbol", }, - "debug/plan9obj": []string{ + "debug/plan9obj": { "ErrNoSymbols", "File", "FileHeader", @@ -2567,16 +2641,16 @@ var stdlib = map[string][]string{ "SectionHeader", "Sym", }, - "embed": []string{ + "embed": { "FS", }, - "encoding": []string{ + "encoding": { "BinaryMarshaler", "BinaryUnmarshaler", "TextMarshaler", "TextUnmarshaler", }, - "encoding/ascii85": []string{ + "encoding/ascii85": { "CorruptInputError", "Decode", "Encode", @@ -2584,7 +2658,7 @@ var stdlib = map[string][]string{ "NewDecoder", "NewEncoder", }, - "encoding/asn1": []string{ + "encoding/asn1": { "BitString", "ClassApplication", "ClassContextSpecific", @@ -2622,7 +2696,7 @@ var stdlib = map[string][]string{ "Unmarshal", "UnmarshalWithParams", }, - "encoding/base32": []string{ + "encoding/base32": { "CorruptInputError", "Encoding", "HexEncoding", @@ -2633,7 +2707,7 @@ var stdlib = map[string][]string{ "StdEncoding", "StdPadding", }, - "encoding/base64": []string{ + "encoding/base64": { "CorruptInputError", "Encoding", "NewDecoder", @@ -2646,7 +2720,10 @@ var stdlib = map[string][]string{ "StdPadding", "URLEncoding", }, - "encoding/binary": []string{ + "encoding/binary": { + "AppendByteOrder", + "AppendUvarint", + "AppendVarint", "BigEndian", "ByteOrder", "LittleEndian", @@ -2663,7 +2740,7 @@ var stdlib = map[string][]string{ "Varint", "Write", }, - "encoding/csv": []string{ + "encoding/csv": { "ErrBareQuote", "ErrFieldCount", "ErrQuote", @@ -2674,7 +2751,7 @@ var stdlib = map[string][]string{ "Reader", "Writer", }, - "encoding/gob": []string{ + "encoding/gob": { "CommonType", "Decoder", "Encoder", @@ -2685,7 +2762,7 @@ var stdlib = map[string][]string{ "Register", "RegisterName", }, - "encoding/hex": []string{ + "encoding/hex": { "Decode", "DecodeString", "DecodedLen", @@ -2699,7 +2776,7 @@ var stdlib = map[string][]string{ "NewDecoder", "NewEncoder", }, - "encoding/json": []string{ + "encoding/json": { "Compact", "Decoder", "Delim", @@ -2726,13 +2803,13 @@ var stdlib = map[string][]string{ "UnsupportedValueError", "Valid", }, - "encoding/pem": []string{ + "encoding/pem": { "Block", "Decode", "Encode", "EncodeToMemory", }, - "encoding/xml": []string{ + "encoding/xml": { "Attr", "CharData", "Comment", @@ -2766,13 +2843,13 @@ var stdlib = map[string][]string{ "UnmarshalerAttr", "UnsupportedTypeError", }, - "errors": []string{ + "errors": { "As", "Is", "New", "Unwrap", }, - "expvar": []string{ + "expvar": { "Do", "Float", "Func", @@ -2789,7 +2866,7 @@ var stdlib = map[string][]string{ "String", "Var", }, - "flag": []string{ + "flag": { "Arg", "Args", "Bool", @@ -2822,6 +2899,7 @@ var stdlib = map[string][]string{ "Set", "String", "StringVar", + "TextVar", "Uint", "Uint64", "Uint64Var", @@ -2833,7 +2911,10 @@ var stdlib = map[string][]string{ "Visit", "VisitAll", }, - "fmt": []string{ + "fmt": { + "Append", + "Appendf", + "Appendln", "Errorf", "Formatter", "Fprint", @@ -2860,7 +2941,7 @@ var stdlib = map[string][]string{ "State", "Stringer", }, - "go/ast": []string{ + "go/ast": { "ArrayType", "AssignStmt", "Bad", @@ -2963,7 +3044,7 @@ var stdlib = map[string][]string{ "Visitor", "Walk", }, - "go/build": []string{ + "go/build": { "AllowBinary", "ArchChar", "Context", @@ -2980,7 +3061,7 @@ var stdlib = map[string][]string{ "Package", "ToolDir", }, - "go/build/constraint": []string{ + "go/build/constraint": { "AndExpr", "Expr", "IsGoBuild", @@ -2992,7 +3073,7 @@ var stdlib = map[string][]string{ "SyntaxError", "TagExpr", }, - "go/constant": []string{ + "go/constant": { "BinaryOp", "BitLen", "Bool", @@ -3033,7 +3114,7 @@ var stdlib = map[string][]string{ "Val", "Value", }, - "go/doc": []string{ + "go/doc": { "AllDecls", "AllMethods", "Example", @@ -3054,17 +3135,35 @@ var stdlib = map[string][]string{ "Type", "Value", }, - "go/format": []string{ + "go/doc/comment": { + "Block", + "Code", + "DefaultLookupPackage", + "Doc", + "DocLink", + "Heading", + "Italic", + "Link", + "LinkDef", + "List", + "ListItem", + "Paragraph", + "Parser", + "Plain", + "Printer", + "Text", + }, + "go/format": { "Node", "Source", }, - "go/importer": []string{ + "go/importer": { "Default", "For", "ForCompiler", "Lookup", }, - "go/parser": []string{ + "go/parser": { "AllErrors", "DeclarationErrors", "ImportsOnly", @@ -3079,7 +3178,7 @@ var stdlib = map[string][]string{ "SpuriousErrors", "Trace", }, - "go/printer": []string{ + "go/printer": { "CommentedNode", "Config", "Fprint", @@ -3089,7 +3188,7 @@ var stdlib = map[string][]string{ "TabIndent", "UseSpaces", }, - "go/scanner": []string{ + "go/scanner": { "Error", "ErrorHandler", "ErrorList", @@ -3098,7 +3197,7 @@ var stdlib = map[string][]string{ "ScanComments", "Scanner", }, - "go/token": []string{ + "go/token": { "ADD", "ADD_ASSIGN", "AND", @@ -3196,7 +3295,7 @@ var stdlib = map[string][]string{ "XOR", "XOR_ASSIGN", }, - "go/types": []string{ + "go/types": { "ArgumentError", "Array", "AssertableTo", @@ -3347,17 +3446,17 @@ var stdlib = map[string][]string{ "WriteSignature", "WriteType", }, - "hash": []string{ + "hash": { "Hash", "Hash32", "Hash64", }, - "hash/adler32": []string{ + "hash/adler32": { "Checksum", "New", "Size", }, - "hash/crc32": []string{ + "hash/crc32": { "Castagnoli", "Checksum", "ChecksumIEEE", @@ -3371,7 +3470,7 @@ var stdlib = map[string][]string{ "Table", "Update", }, - "hash/crc64": []string{ + "hash/crc64": { "Checksum", "ECMA", "ISO", @@ -3381,7 +3480,7 @@ var stdlib = map[string][]string{ "Table", "Update", }, - "hash/fnv": []string{ + "hash/fnv": { "New128", "New128a", "New32", @@ -3389,16 +3488,18 @@ var stdlib = map[string][]string{ "New64", "New64a", }, - "hash/maphash": []string{ + "hash/maphash": { + "Bytes", "Hash", "MakeSeed", "Seed", + "String", }, - "html": []string{ + "html": { "EscapeString", "UnescapeString", }, - "html/template": []string{ + "html/template": { "CSS", "ErrAmbigContext", "ErrBadHTML", @@ -3436,7 +3537,7 @@ var stdlib = map[string][]string{ "URL", "URLQueryEscaper", }, - "image": []string{ + "image": { "Alpha", "Alpha16", "Black", @@ -3489,7 +3590,7 @@ var stdlib = map[string][]string{ "ZP", "ZR", }, - "image/color": []string{ + "image/color": { "Alpha", "Alpha16", "Alpha16Model", @@ -3525,11 +3626,11 @@ var stdlib = map[string][]string{ "YCbCrModel", "YCbCrToRGB", }, - "image/color/palette": []string{ + "image/color/palette": { "Plan9", "WebSafe", }, - "image/draw": []string{ + "image/draw": { "Draw", "DrawMask", "Drawer", @@ -3541,7 +3642,7 @@ var stdlib = map[string][]string{ "RGBA64Image", "Src", }, - "image/gif": []string{ + "image/gif": { "Decode", "DecodeAll", "DecodeConfig", @@ -3553,7 +3654,7 @@ var stdlib = map[string][]string{ "GIF", "Options", }, - "image/jpeg": []string{ + "image/jpeg": { "Decode", "DecodeConfig", "DefaultQuality", @@ -3563,7 +3664,7 @@ var stdlib = map[string][]string{ "Reader", "UnsupportedError", }, - "image/png": []string{ + "image/png": { "BestCompression", "BestSpeed", "CompressionLevel", @@ -3578,11 +3679,11 @@ var stdlib = map[string][]string{ "NoCompression", "UnsupportedError", }, - "index/suffixarray": []string{ + "index/suffixarray": { "Index", "New", }, - "io": []string{ + "io": { "ByteReader", "ByteScanner", "ByteWriter", @@ -3634,7 +3735,7 @@ var stdlib = map[string][]string{ "WriterAt", "WriterTo", }, - "io/fs": []string{ + "io/fs": { "DirEntry", "ErrClosed", "ErrExist", @@ -3678,7 +3779,7 @@ var stdlib = map[string][]string{ "WalkDir", "WalkDirFunc", }, - "io/ioutil": []string{ + "io/ioutil": { "Discard", "NopCloser", "ReadAll", @@ -3688,7 +3789,7 @@ var stdlib = map[string][]string{ "TempFile", "WriteFile", }, - "log": []string{ + "log": { "Default", "Fatal", "Fatalf", @@ -3717,7 +3818,7 @@ var stdlib = map[string][]string{ "SetPrefix", "Writer", }, - "log/syslog": []string{ + "log/syslog": { "Dial", "LOG_ALERT", "LOG_AUTH", @@ -3752,7 +3853,7 @@ var stdlib = map[string][]string{ "Priority", "Writer", }, - "math": []string{ + "math": { "Abs", "Acos", "Acosh", @@ -3851,7 +3952,7 @@ var stdlib = map[string][]string{ "Y1", "Yn", }, - "math/big": []string{ + "math/big": { "Above", "Accuracy", "AwayFromZero", @@ -3878,7 +3979,7 @@ var stdlib = map[string][]string{ "ToZero", "Word", }, - "math/bits": []string{ + "math/bits": { "Add", "Add32", "Add64", @@ -3930,7 +4031,7 @@ var stdlib = map[string][]string{ "TrailingZeros8", "UintSize", }, - "math/cmplx": []string{ + "math/cmplx": { "Abs", "Acos", "Acosh", @@ -3959,7 +4060,7 @@ var stdlib = map[string][]string{ "Tan", "Tanh", }, - "math/rand": []string{ + "math/rand": { "ExpFloat64", "Float32", "Float64", @@ -3984,7 +4085,7 @@ var stdlib = map[string][]string{ "Uint64", "Zipf", }, - "mime": []string{ + "mime": { "AddExtensionType", "BEncoding", "ErrInvalidMediaParameter", @@ -3996,7 +4097,7 @@ var stdlib = map[string][]string{ "WordDecoder", "WordEncoder", }, - "mime/multipart": []string{ + "mime/multipart": { "ErrMessageTooLarge", "File", "FileHeader", @@ -4007,13 +4108,13 @@ var stdlib = map[string][]string{ "Reader", "Writer", }, - "mime/quotedprintable": []string{ + "mime/quotedprintable": { "NewReader", "NewWriter", "Reader", "Writer", }, - "net": []string{ + "net": { "Addr", "AddrError", "Buffers", @@ -4115,7 +4216,7 @@ var stdlib = map[string][]string{ "UnixListener", "UnknownNetworkError", }, - "net/http": []string{ + "net/http": { "AllowQuerySemicolons", "CanonicalHeaderKey", "Client", @@ -4168,6 +4269,7 @@ var stdlib = map[string][]string{ "ListenAndServe", "ListenAndServeTLS", "LocalAddrContextKey", + "MaxBytesError", "MaxBytesHandler", "MaxBytesReader", "MethodConnect", @@ -4290,25 +4392,25 @@ var stdlib = map[string][]string{ "TrailerPrefix", "Transport", }, - "net/http/cgi": []string{ + "net/http/cgi": { "Handler", "Request", "RequestFromMap", "Serve", }, - "net/http/cookiejar": []string{ + "net/http/cookiejar": { "Jar", "New", "Options", "PublicSuffixList", }, - "net/http/fcgi": []string{ + "net/http/fcgi": { "ErrConnClosed", "ErrRequestAborted", "ProcessEnv", "Serve", }, - "net/http/httptest": []string{ + "net/http/httptest": { "DefaultRemoteAddr", "NewRecorder", "NewRequest", @@ -4318,7 +4420,7 @@ var stdlib = map[string][]string{ "ResponseRecorder", "Server", }, - "net/http/httptrace": []string{ + "net/http/httptrace": { "ClientTrace", "ContextClientTrace", "DNSDoneInfo", @@ -4327,7 +4429,7 @@ var stdlib = map[string][]string{ "WithClientTrace", "WroteRequestInfo", }, - "net/http/httputil": []string{ + "net/http/httputil": { "BufferPool", "ClientConn", "DumpRequest", @@ -4346,7 +4448,7 @@ var stdlib = map[string][]string{ "ReverseProxy", "ServerConn", }, - "net/http/pprof": []string{ + "net/http/pprof": { "Cmdline", "Handler", "Index", @@ -4354,7 +4456,7 @@ var stdlib = map[string][]string{ "Symbol", "Trace", }, - "net/mail": []string{ + "net/mail": { "Address", "AddressParser", "ErrHeaderNotPresent", @@ -4365,7 +4467,7 @@ var stdlib = map[string][]string{ "ParseDate", "ReadMessage", }, - "net/netip": []string{ + "net/netip": { "Addr", "AddrFrom16", "AddrFrom4", @@ -4384,7 +4486,7 @@ var stdlib = map[string][]string{ "Prefix", "PrefixFrom", }, - "net/rpc": []string{ + "net/rpc": { "Accept", "Call", "Client", @@ -4411,14 +4513,14 @@ var stdlib = map[string][]string{ "ServerCodec", "ServerError", }, - "net/rpc/jsonrpc": []string{ + "net/rpc/jsonrpc": { "Dial", "NewClient", "NewClientCodec", "NewServerCodec", "ServeConn", }, - "net/smtp": []string{ + "net/smtp": { "Auth", "CRAMMD5Auth", "Client", @@ -4428,7 +4530,7 @@ var stdlib = map[string][]string{ "SendMail", "ServerInfo", }, - "net/textproto": []string{ + "net/textproto": { "CanonicalMIMEHeaderKey", "Conn", "Dial", @@ -4444,10 +4546,11 @@ var stdlib = map[string][]string{ "TrimString", "Writer", }, - "net/url": []string{ + "net/url": { "Error", "EscapeError", "InvalidHostError", + "JoinPath", "Parse", "ParseQuery", "ParseRequestURI", @@ -4461,7 +4564,7 @@ var stdlib = map[string][]string{ "Userinfo", "Values", }, - "os": []string{ + "os": { "Args", "Chdir", "Chmod", @@ -4577,16 +4680,17 @@ var stdlib = map[string][]string{ "UserHomeDir", "WriteFile", }, - "os/exec": []string{ + "os/exec": { "Cmd", "Command", "CommandContext", + "ErrDot", "ErrNotFound", "Error", "ExitError", "LookPath", }, - "os/signal": []string{ + "os/signal": { "Ignore", "Ignored", "Notify", @@ -4594,7 +4698,7 @@ var stdlib = map[string][]string{ "Reset", "Stop", }, - "os/user": []string{ + "os/user": { "Current", "Group", "Lookup", @@ -4607,7 +4711,7 @@ var stdlib = map[string][]string{ "UnknownUserIdError", "User", }, - "path": []string{ + "path": { "Base", "Clean", "Dir", @@ -4618,7 +4722,7 @@ var stdlib = map[string][]string{ "Match", "Split", }, - "path/filepath": []string{ + "path/filepath": { "Abs", "Base", "Clean", @@ -4644,12 +4748,12 @@ var stdlib = map[string][]string{ "WalkDir", "WalkFunc", }, - "plugin": []string{ + "plugin": { "Open", "Plugin", "Symbol", }, - "reflect": []string{ + "reflect": { "Append", "AppendSlice", "Array", @@ -4724,7 +4828,7 @@ var stdlib = map[string][]string{ "VisibleFields", "Zero", }, - "regexp": []string{ + "regexp": { "Compile", "CompilePOSIX", "Match", @@ -4735,7 +4839,7 @@ var stdlib = map[string][]string{ "QuoteMeta", "Regexp", }, - "regexp/syntax": []string{ + "regexp/syntax": { "ClassNL", "Compile", "DotNL", @@ -4759,6 +4863,7 @@ var stdlib = map[string][]string{ "ErrMissingBracket", "ErrMissingParen", "ErrMissingRepeatArgument", + "ErrNestingDepth", "ErrTrailingBackslash", "ErrUnexpectedParen", "Error", @@ -4813,7 +4918,7 @@ var stdlib = map[string][]string{ "UnicodeGroups", "WasDollar", }, - "runtime": []string{ + "runtime": { "BlockProfile", "BlockProfileRecord", "Breakpoint", @@ -4861,11 +4966,11 @@ var stdlib = map[string][]string{ "UnlockOSThread", "Version", }, - "runtime/cgo": []string{ + "runtime/cgo": { "Handle", "NewHandle", }, - "runtime/debug": []string{ + "runtime/debug": { "BuildInfo", "BuildSetting", "FreeOSMemory", @@ -4878,12 +4983,13 @@ var stdlib = map[string][]string{ "SetGCPercent", "SetMaxStack", "SetMaxThreads", + "SetMemoryLimit", "SetPanicOnFault", "SetTraceback", "Stack", "WriteHeapDump", }, - "runtime/metrics": []string{ + "runtime/metrics": { "All", "Description", "Float64Histogram", @@ -4896,7 +5002,7 @@ var stdlib = map[string][]string{ "Value", "ValueKind", }, - "runtime/pprof": []string{ + "runtime/pprof": { "Do", "ForLabels", "Label", @@ -4912,7 +5018,7 @@ var stdlib = map[string][]string{ "WithLabels", "WriteHeapProfile", }, - "runtime/trace": []string{ + "runtime/trace": { "IsEnabled", "Log", "Logf", @@ -4924,7 +5030,8 @@ var stdlib = map[string][]string{ "Task", "WithRegion", }, - "sort": []string{ + "sort": { + "Find", "Float64Slice", "Float64s", "Float64sAreSorted", @@ -4947,7 +5054,7 @@ var stdlib = map[string][]string{ "Strings", "StringsAreSorted", }, - "strconv": []string{ + "strconv": { "AppendBool", "AppendFloat", "AppendInt", @@ -4987,7 +5094,7 @@ var stdlib = map[string][]string{ "Unquote", "UnquoteChar", }, - "strings": []string{ + "strings": { "Builder", "Clone", "Compare", @@ -5041,7 +5148,7 @@ var stdlib = map[string][]string{ "TrimSpace", "TrimSuffix", }, - "sync": []string{ + "sync": { "Cond", "Locker", "Map", @@ -5052,24 +5159,28 @@ var stdlib = map[string][]string{ "RWMutex", "WaitGroup", }, - "sync/atomic": []string{ + "sync/atomic": { "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr", + "Bool", "CompareAndSwapInt32", "CompareAndSwapInt64", "CompareAndSwapPointer", "CompareAndSwapUint32", "CompareAndSwapUint64", "CompareAndSwapUintptr", + "Int32", + "Int64", "LoadInt32", "LoadInt64", "LoadPointer", "LoadUint32", "LoadUint64", "LoadUintptr", + "Pointer", "StoreInt32", "StoreInt64", "StorePointer", @@ -5082,9 +5193,12 @@ var stdlib = map[string][]string{ "SwapUint32", "SwapUint64", "SwapUintptr", + "Uint32", + "Uint64", + "Uintptr", "Value", }, - "syscall": []string{ + "syscall": { "AF_ALG", "AF_APPLETALK", "AF_ARP", @@ -10234,7 +10348,7 @@ var stdlib = map[string][]string{ "XP1_UNI_RECV", "XP1_UNI_SEND", }, - "syscall/js": []string{ + "syscall/js": { "CopyBytesToGo", "CopyBytesToJS", "Error", @@ -10256,7 +10370,7 @@ var stdlib = map[string][]string{ "ValueError", "ValueOf", }, - "testing": []string{ + "testing": { "AllocsPerRun", "B", "Benchmark", @@ -10284,12 +10398,12 @@ var stdlib = map[string][]string{ "TB", "Verbose", }, - "testing/fstest": []string{ + "testing/fstest": { "MapFS", "MapFile", "TestFS", }, - "testing/iotest": []string{ + "testing/iotest": { "DataErrReader", "ErrReader", "ErrTimeout", @@ -10301,7 +10415,7 @@ var stdlib = map[string][]string{ "TimeoutReader", "TruncateWriter", }, - "testing/quick": []string{ + "testing/quick": { "Check", "CheckEqual", "CheckEqualError", @@ -10311,7 +10425,7 @@ var stdlib = map[string][]string{ "SetupError", "Value", }, - "text/scanner": []string{ + "text/scanner": { "Char", "Comment", "EOF", @@ -10334,7 +10448,7 @@ var stdlib = map[string][]string{ "String", "TokenString", }, - "text/tabwriter": []string{ + "text/tabwriter": { "AlignRight", "Debug", "DiscardEmptyColumns", @@ -10345,7 +10459,7 @@ var stdlib = map[string][]string{ "TabIndent", "Writer", }, - "text/template": []string{ + "text/template": { "ExecError", "FuncMap", "HTMLEscape", @@ -10363,7 +10477,7 @@ var stdlib = map[string][]string{ "Template", "URLQueryEscaper", }, - "text/template/parse": []string{ + "text/template/parse": { "ActionNode", "BoolNode", "BranchNode", @@ -10419,7 +10533,7 @@ var stdlib = map[string][]string{ "VariableNode", "WithNode", }, - "time": []string{ + "time": { "ANSIC", "After", "AfterFunc", @@ -10491,7 +10605,7 @@ var stdlib = map[string][]string{ "Wednesday", "Weekday", }, - "unicode": []string{ + "unicode": { "ASCII_Hex_Digit", "Adlam", "Ahom", @@ -10777,14 +10891,14 @@ var stdlib = map[string][]string{ "Zp", "Zs", }, - "unicode/utf16": []string{ + "unicode/utf16": { "Decode", "DecodeRune", "Encode", "EncodeRune", "IsSurrogate", }, - "unicode/utf8": []string{ + "unicode/utf8": { "AppendRune", "DecodeLastRune", "DecodeLastRuneInString", @@ -10805,7 +10919,7 @@ var stdlib = map[string][]string{ "ValidRune", "ValidString", }, - "unsafe": []string{ + "unsafe": { "Alignof", "ArbitraryType", "Offsetof", diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index ca7752d664a..529cfa5ded3 100644 --- a/internal/jsonrpc2/conn.go +++ b/internal/jsonrpc2/conn.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) // Conn is the common interface to jsonrpc clients and servers. diff --git a/internal/jsonrpc2/serve.go b/internal/jsonrpc2/serve.go index 4181bf10c33..cfbcbcb021c 100644 --- a/internal/jsonrpc2/serve.go +++ b/internal/jsonrpc2/serve.go @@ -8,6 +8,7 @@ import ( "context" "errors" "io" + "math" "net" "os" "time" @@ -100,7 +101,7 @@ func Serve(ctx context.Context, ln net.Listener, server StreamServer, idleTimeou }() // Max duration: ~290 years; surely that's long enough. - const forever = 1<<63 - 1 + const forever = math.MaxInt64 if idleTimeout <= 0 { idleTimeout = forever } diff --git a/internal/jsonrpc2/servertest/servertest.go b/internal/jsonrpc2/servertest/servertest.go index 392e084a9ad..37f8475bee2 100644 --- a/internal/jsonrpc2/servertest/servertest.go +++ b/internal/jsonrpc2/servertest/servertest.go @@ -50,7 +50,7 @@ func NewTCPServer(ctx context.Context, server jsonrpc2.StreamServer, framer json // Connect dials the test server and returns a jsonrpc2 Connection that is // ready for use. -func (s *TCPServer) Connect(ctx context.Context) jsonrpc2.Conn { +func (s *TCPServer) Connect(_ context.Context) jsonrpc2.Conn { netConn, err := net.Dial("tcp", s.Addr) if err != nil { panic(fmt.Sprintf("servertest: failed to connect to test instance: %v", err)) @@ -68,7 +68,7 @@ type PipeServer struct { } // NewPipeServer returns a test server that can be connected to via io.Pipes. -func NewPipeServer(ctx context.Context, server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *PipeServer { +func NewPipeServer(server jsonrpc2.StreamServer, framer jsonrpc2.Framer) *PipeServer { if framer == nil { framer = jsonrpc2.NewRawStream } diff --git a/internal/jsonrpc2/servertest/servertest_test.go b/internal/jsonrpc2/servertest/servertest_test.go index 38fa21a24d9..1780d4f9147 100644 --- a/internal/jsonrpc2/servertest/servertest_test.go +++ b/internal/jsonrpc2/servertest/servertest_test.go @@ -26,7 +26,7 @@ func TestTestServer(t *testing.T) { server := jsonrpc2.HandlerServer(fakeHandler) tcpTS := NewTCPServer(ctx, server, nil) defer tcpTS.Close() - pipeTS := NewPipeServer(ctx, server, nil) + pipeTS := NewPipeServer(server, nil) defer pipeTS.Close() tests := []struct { diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index edcf0939f2a..60afa7060e4 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go @@ -7,13 +7,17 @@ package jsonrpc2 import ( "context" "encoding/json" + "errors" "fmt" "io" + "sync" "sync/atomic" + "time" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/lsp/debug/tag" + "golang.org/x/tools/internal/event/tag" ) // Binder builds a connection configuration. @@ -23,10 +27,21 @@ import ( type Binder interface { // Bind returns the ConnectionOptions to use when establishing the passed-in // Connection. - // The connection is not ready to use when Bind is called. - Bind(context.Context, *Connection) (ConnectionOptions, error) + // + // The connection is not ready to use when Bind is called, + // but Bind may close it without reading or writing to it. + Bind(context.Context, *Connection) ConnectionOptions } +// A BinderFunc implements the Binder interface for a standalone Bind function. +type BinderFunc func(context.Context, *Connection) ConnectionOptions + +func (f BinderFunc) Bind(ctx context.Context, c *Connection) ConnectionOptions { + return f(ctx, c) +} + +var _ Binder = BinderFunc(nil) + // ConnectionOptions holds the options for new connections. type ConnectionOptions struct { // Framer allows control over the message framing and encoding. @@ -38,6 +53,10 @@ type ConnectionOptions struct { // Handler is used as the queued message handler for inbound messages. // If nil, all responses will be ErrNotHandled. Handler Handler + // OnInternalError, if non-nil, is called with any internal errors that occur + // while serving the connection, such as protocol errors or invariant + // violations. (If nil, internal errors result in panics.) + OnInternalError func(error) } // Connection manages the jsonrpc2 protocol, connecting responses back to their @@ -45,102 +64,244 @@ type ConnectionOptions struct { // Connection is bidirectional; it does not have a designated server or client // end. type Connection struct { - seq int64 // must only be accessed using atomic operations - closer io.Closer - writerBox chan Writer - outgoingBox chan map[ID]chan<- *Response - incomingBox chan map[ID]*incoming - async *async + seq int64 // must only be accessed using atomic operations + + stateMu sync.Mutex + state inFlightState // accessed only in updateInFlight + done chan struct{} // closed (under stateMu) when state.closed is true and all goroutines have completed + + writer chan Writer // 1-buffered; stores the writer when not in use + + handler Handler + + onInternalError func(error) + onDone func() } -type AsyncCall struct { - id ID - response chan *Response // the channel a response will be delivered on - resultBox chan asyncResult - endSpan func() // close the tracing span when all processing for the message is complete +// inFlightState records the state of the incoming and outgoing calls on a +// Connection. +type inFlightState struct { + connClosing bool // true when the Connection's Close method has been called + reading bool // true while the readIncoming goroutine is running + readErr error // non-nil when the readIncoming goroutine exits (typically io.EOF) + writeErr error // non-nil if a call to the Writer has failed with a non-canceled Context + + // closer shuts down and cleans up the Reader and Writer state, ideally + // interrupting any Read or Write call that is currently blocked. It is closed + // when the state is idle and one of: connClosing is true, readErr is non-nil, + // or writeErr is non-nil. + // + // After the closer has been invoked, the closer field is set to nil + // and the closeErr field is simultaneously set to its result. + closer io.Closer + closeErr error // error returned from closer.Close + + outgoingCalls map[ID]*AsyncCall // calls only + outgoingNotifications int // # of notifications awaiting "write" + + // incoming stores the total number of incoming calls and notifications + // that have not yet written or processed a result. + incoming int + + incomingByID map[ID]*incomingRequest // calls only + + // handlerQueue stores the backlog of calls and notifications that were not + // already handled by a preempter. + // The queue does not include the request currently being handled (if any). + handlerQueue []*incomingRequest + handlerRunning bool +} + +// updateInFlight locks the state of the connection's in-flight requests, allows +// f to mutate that state, and closes the connection if it is idle and either +// is closing or has a read or write error. +func (c *Connection) updateInFlight(f func(*inFlightState)) { + c.stateMu.Lock() + defer c.stateMu.Unlock() + + s := &c.state + + f(s) + + select { + case <-c.done: + // The connection was already completely done at the start of this call to + // updateInFlight, so it must remain so. (The call to f should have noticed + // that and avoided making any updates that would cause the state to be + // non-idle.) + if !s.idle() { + panic("jsonrpc2_v2: updateInFlight transitioned to non-idle when already done") + } + return + default: + } + + if s.idle() && s.shuttingDown(ErrUnknown) != nil { + if s.closer != nil { + s.closeErr = s.closer.Close() + s.closer = nil // prevent duplicate Close calls + } + if s.reading { + // The readIncoming goroutine is still running. Our call to Close should + // cause it to exit soon, at which point it will make another call to + // updateInFlight, set s.reading to false, and mark the Connection done. + } else { + // The readIncoming goroutine has exited, or never started to begin with. + // Since everything else is idle, we're completely done. + if c.onDone != nil { + c.onDone() + } + close(c.done) + } + } } -type asyncResult struct { - result []byte - err error +// idle reports whether the connction is in a state with no pending calls or +// notifications. +// +// If idle returns true, the readIncoming goroutine may still be running, +// but no other goroutines are doing work on behalf of the connnection. +func (s *inFlightState) idle() bool { + return len(s.outgoingCalls) == 0 && s.outgoingNotifications == 0 && s.incoming == 0 && !s.handlerRunning } -// incoming is used to track an incoming request as it is being handled -type incoming struct { - request *Request // the request being processed - baseCtx context.Context // a base context for the message processing - done func() // a function called when all processing for the message is complete - handleCtx context.Context // the context for handling the message, child of baseCtx - cancel func() // a function that cancels the handling context +// shuttingDown reports whether the connection is in a state that should +// disallow new (incoming and outgoing) calls. It returns either nil or +// an error that is or wraps the provided errClosing. +func (s *inFlightState) shuttingDown(errClosing error) error { + if s.connClosing { + // If Close has been called explicitly, it doesn't matter what state the + // Reader and Writer are in: we shouldn't be starting new work because the + // caller told us not to start new work. + return errClosing + } + if s.readErr != nil { + // If the read side of the connection is broken, we cannot read new call + // requests, and cannot read responses to our outgoing calls. + return fmt.Errorf("%w: %v", errClosing, s.readErr) + } + if s.writeErr != nil { + // If the write side of the connection is broken, we cannot write responses + // for incoming calls, and cannot write requests for outgoing calls. + return fmt.Errorf("%w: %v", errClosing, s.writeErr) + } + return nil +} + +// incomingRequest is used to track an incoming request as it is being handled +type incomingRequest struct { + *Request // the request being processed + ctx context.Context + cancel context.CancelFunc + endSpan func() // called (and set to nil) when the response is sent } // Bind returns the options unmodified. -func (o ConnectionOptions) Bind(context.Context, *Connection) (ConnectionOptions, error) { - return o, nil +func (o ConnectionOptions) Bind(context.Context, *Connection) ConnectionOptions { + return o } // newConnection creates a new connection and runs it. +// // This is used by the Dial and Serve functions to build the actual connection. -func newConnection(ctx context.Context, rwc io.ReadWriteCloser, binder Binder) (*Connection, error) { +// +// The connection is closed automatically (and its resources cleaned up) when +// the last request has completed after the underlying ReadWriteCloser breaks, +// but it may be stopped earlier by calling Close (for a clean shutdown). +func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder, onDone func()) *Connection { + // TODO: Should we create a new event span here? + // This will propagate cancellation from ctx; should it? + ctx := notDone{bindCtx} + c := &Connection{ - closer: rwc, - writerBox: make(chan Writer, 1), - outgoingBox: make(chan map[ID]chan<- *Response, 1), - incomingBox: make(chan map[ID]*incoming, 1), - async: newAsync(), + state: inFlightState{closer: rwc}, + done: make(chan struct{}), + writer: make(chan Writer, 1), + onDone: onDone, } + // It's tempting to set a finalizer on c to verify that the state has gone + // idle when the connection becomes unreachable. Unfortunately, the Binder + // interface makes that unsafe: it allows the Handler to close over the + // Connection, which could create a reference cycle that would cause the + // Connection to become uncollectable. - options, err := binder.Bind(ctx, c) - if err != nil { - return nil, err - } - if options.Framer == nil { - options.Framer = HeaderFramer() - } - if options.Preempter == nil { - options.Preempter = defaultHandler{} - } - if options.Handler == nil { - options.Handler = defaultHandler{} - } - c.outgoingBox <- make(map[ID]chan<- *Response) - c.incomingBox <- make(map[ID]*incoming) - // the goroutines started here will continue until the underlying stream is closed - reader := options.Framer.Reader(rwc) - readToQueue := make(chan *incoming) - queueToDeliver := make(chan *incoming) - go c.readIncoming(ctx, reader, readToQueue) - go c.manageQueue(ctx, options.Preempter, readToQueue, queueToDeliver) - go c.deliverMessages(ctx, options.Handler, queueToDeliver) - - // releaseing the writer must be the last thing we do in case any requests - // are blocked waiting for the connection to be ready - c.writerBox <- options.Framer.Writer(rwc) - return c, nil + options := binder.Bind(bindCtx, c) + framer := options.Framer + if framer == nil { + framer = HeaderFramer() + } + c.handler = options.Handler + if c.handler == nil { + c.handler = defaultHandler{} + } + c.onInternalError = options.OnInternalError + + c.writer <- framer.Writer(rwc) + reader := framer.Reader(rwc) + + c.updateInFlight(func(s *inFlightState) { + select { + case <-c.done: + // Bind already closed the connection; don't start a goroutine to read it. + return + default: + } + + // The goroutine started here will continue until the underlying stream is closed. + // + // (If the Binder closed the Connection already, this should error out and + // return almost immediately.) + s.reading = true + go c.readIncoming(ctx, reader, options.Preempter) + }) + return c } // Notify invokes the target method but does not wait for a response. // The params will be marshaled to JSON before sending over the wire, and will // be handed to the method invoked. -func (c *Connection) Notify(ctx context.Context, method string, params interface{}) error { - notify, err := NewNotification(method, params) - if err != nil { - return fmt.Errorf("marshaling notify parameters: %v", err) - } +func (c *Connection) Notify(ctx context.Context, method string, params interface{}) (err error) { ctx, done := event.Start(ctx, method, tag.Method.Of(method), tag.RPCDirection.Of(tag.Outbound), ) - event.Metric(ctx, tag.Started.Of(1)) - err = c.write(ctx, notify) - switch { - case err != nil: - event.Label(ctx, tag.StatusCode.Of("ERROR")) - default: - event.Label(ctx, tag.StatusCode.Of("OK")) + attempted := false + + defer func() { + labelStatus(ctx, err) + done() + if attempted { + c.updateInFlight(func(s *inFlightState) { + s.outgoingNotifications-- + }) + } + }() + + c.updateInFlight(func(s *inFlightState) { + // If the connection is shutting down, allow outgoing notifications only if + // there is at least one call still in flight. The number of calls in flight + // cannot increase once shutdown begins, and allowing outgoing notifications + // may permit notifications that will cancel in-flight calls. + if len(s.outgoingCalls) == 0 && len(s.incomingByID) == 0 { + err = s.shuttingDown(ErrClientClosing) + if err != nil { + return + } + } + s.outgoingNotifications++ + attempted = true + }) + if err != nil { + return err } - done() - return err + + notify, err := NewNotification(method, params) + if err != nil { + return fmt.Errorf("marshaling notify parameters: %v", err) + } + + event.Metric(ctx, tag.Started.Of(1)) + return c.write(ctx, notify) } // Call invokes the target method and returns an object that can be used to await the response. @@ -149,339 +310,503 @@ func (c *Connection) Notify(ctx context.Context, method string, params interface // You do not have to wait for the response, it can just be ignored if not needed. // If sending the call failed, the response will be ready and have the error in it. func (c *Connection) Call(ctx context.Context, method string, params interface{}) *AsyncCall { - result := &AsyncCall{ - id: Int64ID(atomic.AddInt64(&c.seq, 1)), - resultBox: make(chan asyncResult, 1), - } - // generate a new request identifier - call, err := NewCall(result.id, method, params) - if err != nil { - //set the result to failed - result.resultBox <- asyncResult{err: fmt.Errorf("marshaling call parameters: %w", err)} - return result - } + // Generate a new request identifier. + id := Int64ID(atomic.AddInt64(&c.seq, 1)) ctx, endSpan := event.Start(ctx, method, tag.Method.Of(method), tag.RPCDirection.Of(tag.Outbound), - tag.RPCID.Of(fmt.Sprintf("%q", result.id)), + tag.RPCID.Of(fmt.Sprintf("%q", id)), ) - result.endSpan = endSpan + + ac := &AsyncCall{ + id: id, + ready: make(chan struct{}), + ctx: ctx, + endSpan: endSpan, + } + // When this method returns, either ac is retired, or the request has been + // written successfully and the call is awaiting a response (to be provided by + // the readIncoming goroutine). + + call, err := NewCall(ac.id, method, params) + if err != nil { + ac.retire(&Response{ID: id, Error: fmt.Errorf("marshaling call parameters: %w", err)}) + return ac + } + + c.updateInFlight(func(s *inFlightState) { + err = s.shuttingDown(ErrClientClosing) + if err != nil { + return + } + if s.outgoingCalls == nil { + s.outgoingCalls = make(map[ID]*AsyncCall) + } + s.outgoingCalls[ac.id] = ac + }) + if err != nil { + ac.retire(&Response{ID: id, Error: err}) + return ac + } + event.Metric(ctx, tag.Started.Of(1)) - // We have to add ourselves to the pending map before we send, otherwise we - // are racing the response. - // rchan is buffered in case the response arrives without a listener. - result.response = make(chan *Response, 1) - pending := <-c.outgoingBox - pending[result.id] = result.response - c.outgoingBox <- pending - // now we are ready to send if err := c.write(ctx, call); err != nil { - // sending failed, we will never get a response, so deliver a fake one - r, _ := NewResponse(result.id, nil, err) - c.incomingResponse(r) + // Sending failed. We will never get a response, so deliver a fake one if it + // wasn't already retired by the connection breaking. + c.updateInFlight(func(s *inFlightState) { + if s.outgoingCalls[ac.id] == ac { + delete(s.outgoingCalls, ac.id) + ac.retire(&Response{ID: id, Error: err}) + } else { + // ac was already retired by the readIncoming goroutine: + // perhaps our write raced with the Read side of the connection breaking. + } + }) } - return result + return ac +} + +type AsyncCall struct { + id ID + ready chan struct{} // closed after response has been set and span has been ended + response *Response + ctx context.Context // for event logging only + endSpan func() // close the tracing span when all processing for the message is complete } // ID used for this call. // This can be used to cancel the call if needed. -func (a *AsyncCall) ID() ID { return a.id } +func (ac *AsyncCall) ID() ID { return ac.id } // IsReady can be used to check if the result is already prepared. // This is guaranteed to return true on a result for which Await has already // returned, or a call that failed to send in the first place. -func (a *AsyncCall) IsReady() bool { +func (ac *AsyncCall) IsReady() bool { select { - case r := <-a.resultBox: - a.resultBox <- r + case <-ac.ready: return true default: return false } } -// Await the results of a Call. +// retire processes the response to the call. +func (ac *AsyncCall) retire(response *Response) { + select { + case <-ac.ready: + panic(fmt.Sprintf("jsonrpc2: retire called twice for ID %v", ac.id)) + default: + } + + ac.response = response + labelStatus(ac.ctx, response.Error) + ac.endSpan() + // Allow the trace context, which may retain a lot of reachable values, + // to be garbage-collected. + ac.ctx, ac.endSpan = nil, nil + + close(ac.ready) +} + +// Await waits for (and decodes) the results of a Call. // The response will be unmarshaled from JSON into the result. -func (a *AsyncCall) Await(ctx context.Context, result interface{}) error { - defer a.endSpan() - var r asyncResult +func (ac *AsyncCall) Await(ctx context.Context, result interface{}) error { select { - case response := <-a.response: - // response just arrived, prepare the result - switch { - case response.Error != nil: - r.err = response.Error - event.Label(ctx, tag.StatusCode.Of("ERROR")) - default: - r.result = response.Result - event.Label(ctx, tag.StatusCode.Of("OK")) - } - case r = <-a.resultBox: - // result already available case <-ctx.Done(): - event.Label(ctx, tag.StatusCode.Of("CANCELLED")) return ctx.Err() + case <-ac.ready: } - // refill the box for the next caller - a.resultBox <- r - // and unpack the result - if r.err != nil { - return r.err + if ac.response.Error != nil { + return ac.response.Error } - if result == nil || len(r.result) == 0 { + if result == nil { return nil } - return json.Unmarshal(r.result, result) + return json.Unmarshal(ac.response.Result, result) } // Respond delivers a response to an incoming Call. // // Respond must be called exactly once for any message for which a handler // returns ErrAsyncResponse. It must not be called for any other message. -func (c *Connection) Respond(id ID, result interface{}, rerr error) error { - pending := <-c.incomingBox - defer func() { c.incomingBox <- pending }() - entry, found := pending[id] - if !found { - return nil +func (c *Connection) Respond(id ID, result interface{}, err error) error { + var req *incomingRequest + c.updateInFlight(func(s *inFlightState) { + req = s.incomingByID[id] + }) + if req == nil { + return c.internalErrorf("Request not found for ID %v", id) + } + + if err == ErrAsyncResponse { + // Respond is supposed to supply the asynchronous response, so it would be + // confusing to call Respond with an error that promises to call Respond + // again. + err = c.internalErrorf("Respond called with ErrAsyncResponse for %q", req.Method) } - delete(pending, id) - return c.respond(entry, result, rerr) + return c.processResult("Respond", req, result, err) } -// Cancel is used to cancel an inbound message by ID, it does not cancel -// outgoing messages. -// This is only used inside a message handler that is layering a -// cancellation protocol on top of JSON RPC 2. -// It will not complain if the ID is not a currently active message, and it will -// not cause any messages that have not arrived yet with that ID to be +// Cancel cancels the Context passed to the Handle call for the inbound message +// with the given ID. +// +// Cancel will not complain if the ID is not a currently active message, and it +// will not cause any messages that have not arrived yet with that ID to be // cancelled. func (c *Connection) Cancel(id ID) { - pending := <-c.incomingBox - defer func() { c.incomingBox <- pending }() - if entry, found := pending[id]; found && entry.cancel != nil { - entry.cancel() - entry.cancel = nil + var req *incomingRequest + c.updateInFlight(func(s *inFlightState) { + req = s.incomingByID[id] + }) + if req != nil { + req.cancel() } } // Wait blocks until the connection is fully closed, but does not close it. func (c *Connection) Wait() error { - return c.async.wait() + var err error + <-c.done + c.updateInFlight(func(s *inFlightState) { + err = s.closeErr + }) + return err } -// Close can be used to close the underlying stream, and then wait for the connection to -// fully shut down. -// This does not cancel in flight requests, but waits for them to gracefully complete. +// Close stops accepting new requests, waits for in-flight requests and enqueued +// Handle calls to complete, and then closes the underlying stream. +// +// After the start of a Close, notification requests (that lack IDs and do not +// receive responses) will continue to be passed to the Preempter, but calls +// with IDs will receive immediate responses with ErrServerClosing, and no new +// requests (not even notifications!) will be enqueued to the Handler. func (c *Connection) Close() error { - // close the underlying stream - if err := c.closer.Close(); err != nil && !isClosingError(err) { - return err - } - // and then wait for it to cause the connection to close - if err := c.Wait(); err != nil && !isClosingError(err) { - return err - } - return nil + // Stop handling new requests, and interrupt the reader (by closing the + // connection) as soon as the active requests finish. + c.updateInFlight(func(s *inFlightState) { s.connClosing = true }) + + return c.Wait() } // readIncoming collects inbound messages from the reader and delivers them, either responding // to outgoing calls or feeding requests to the queue. -func (c *Connection) readIncoming(ctx context.Context, reader Reader, toQueue chan<- *incoming) { - defer close(toQueue) +func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter Preempter) { + var err error for { - // get the next message - // no lock is needed, this is the only reader - msg, n, err := reader.Read(ctx) + var ( + msg Message + n int64 + ) + msg, n, err = reader.Read(ctx) if err != nil { - // The stream failed, we cannot continue - c.async.setError(err) - return + break } + switch msg := msg.(type) { case *Request: - entry := &incoming{ - request: msg, - } - // add a span to the context for this request - labels := append(make([]label.Label, 0, 3), // make space for the id if present - tag.Method.Of(msg.Method), - tag.RPCDirection.Of(tag.Inbound), - ) - if msg.IsCall() { - labels = append(labels, tag.RPCID.Of(fmt.Sprintf("%q", msg.ID))) - } - entry.baseCtx, entry.done = event.Start(ctx, msg.Method, labels...) - event.Metric(entry.baseCtx, - tag.Started.Of(1), - tag.ReceivedBytes.Of(n)) - // in theory notifications cannot be cancelled, but we build them a cancel context anyway - entry.handleCtx, entry.cancel = context.WithCancel(entry.baseCtx) - // if the request is a call, add it to the incoming map so it can be - // cancelled by id - if msg.IsCall() { - pending := <-c.incomingBox - pending[msg.ID] = entry - c.incomingBox <- pending - } - // send the message to the incoming queue - toQueue <- entry + c.acceptRequest(ctx, msg, n, preempter) + case *Response: - // If method is not set, this should be a response, in which case we must - // have an id to send the response back to the caller. - c.incomingResponse(msg) + c.updateInFlight(func(s *inFlightState) { + if ac, ok := s.outgoingCalls[msg.ID]; ok { + delete(s.outgoingCalls, msg.ID) + ac.retire(msg) + } else { + // TODO: How should we report unexpected responses? + } + }) + + default: + c.internalErrorf("Read returned an unexpected message of type %T", msg) } } + + c.updateInFlight(func(s *inFlightState) { + s.reading = false + s.readErr = err + + // Retire any outgoing requests that were still in flight: with the Reader no + // longer being processed, they necessarily cannot receive a response. + for id, ac := range s.outgoingCalls { + ac.retire(&Response{ID: id, Error: err}) + } + s.outgoingCalls = nil + }) } -func (c *Connection) incomingResponse(msg *Response) { - pending := <-c.outgoingBox - response, ok := pending[msg.ID] - if ok { - delete(pending, msg.ID) +// acceptRequest either handles msg synchronously or enqueues it to be handled +// asynchronously. +func (c *Connection) acceptRequest(ctx context.Context, msg *Request, msgBytes int64, preempter Preempter) { + // Add a span to the context for this request. + labels := append(make([]label.Label, 0, 3), // Make space for the ID if present. + tag.Method.Of(msg.Method), + tag.RPCDirection.Of(tag.Inbound), + ) + if msg.IsCall() { + labels = append(labels, tag.RPCID.Of(fmt.Sprintf("%q", msg.ID))) } - c.outgoingBox <- pending - if response != nil { - response <- msg + ctx, endSpan := event.Start(ctx, msg.Method, labels...) + event.Metric(ctx, + tag.Started.Of(1), + tag.ReceivedBytes.Of(msgBytes)) + + // In theory notifications cannot be cancelled, but we build them a cancel + // context anyway. + ctx, cancel := context.WithCancel(ctx) + req := &incomingRequest{ + Request: msg, + ctx: ctx, + cancel: cancel, + endSpan: endSpan, } -} -// manageQueue reads incoming requests, attempts to process them with the preempter, or queue them -// up for normal handling. -func (c *Connection) manageQueue(ctx context.Context, preempter Preempter, fromRead <-chan *incoming, toDeliver chan<- *incoming) { - defer close(toDeliver) - q := []*incoming{} - ok := true - for { - var nextReq *incoming - if len(q) == 0 { - // no messages in the queue - // if we were closing, then we are done - if !ok { + // If the request is a call, add it to the incoming map so it can be + // cancelled (or responded) by ID. + var err error + c.updateInFlight(func(s *inFlightState) { + s.incoming++ + + if req.IsCall() { + if s.incomingByID[req.ID] != nil { + err = fmt.Errorf("%w: request ID %v already in use", ErrInvalidRequest, req.ID) + req.ID = ID{} // Don't misattribute this error to the existing request. return } - // not closing, but nothing in the queue, so just block waiting for a read - nextReq, ok = <-fromRead - } else { - // we have a non empty queue, so pick whichever of reading or delivering - // that we can make progress on - select { - case nextReq, ok = <-fromRead: - case toDeliver <- q[0]: - //TODO: this causes a lot of shuffling, should we use a growing ring buffer? compaction? - q = q[1:] + + if s.incomingByID == nil { + s.incomingByID = make(map[ID]*incomingRequest) } + s.incomingByID[req.ID] = req + + // When shutting down, reject all new Call requests, even if they could + // theoretically be handled by the preempter. The preempter could return + // ErrAsyncResponse, which would increase the amount of work in flight + // when we're trying to ensure that it strictly decreases. + err = s.shuttingDown(ErrServerClosing) } - if nextReq != nil { - // TODO: should we allow to limit the queue size? - var result interface{} - rerr := nextReq.handleCtx.Err() - if rerr == nil { - // only preempt if not already cancelled - result, rerr = preempter.Preempt(nextReq.handleCtx, nextReq.request) - } - switch { - case rerr == ErrNotHandled: - // message not handled, add it to the queue for the main handler - q = append(q, nextReq) - case rerr == ErrAsyncResponse: - // message handled but the response will come later - default: - // anything else means the message is fully handled - c.reply(nextReq, result, rerr) - } + }) + if err != nil { + c.processResult("acceptRequest", req, nil, err) + return + } + + if preempter != nil { + result, err := preempter.Preempt(req.ctx, req.Request) + + if req.IsCall() && errors.Is(err, ErrAsyncResponse) { + // This request will remain in flight until Respond is called for it. + return + } + + if !errors.Is(err, ErrNotHandled) { + c.processResult("Preempt", req, result, err) + return } } + + c.updateInFlight(func(s *inFlightState) { + // If the connection is shutting down, don't enqueue anything to the + // handler — not even notifications. That ensures that if the handler + // continues to make progress, it will eventually become idle and + // close the connection. + err = s.shuttingDown(ErrServerClosing) + if err != nil { + return + } + + // We enqueue requests that have not been preempted to an unbounded slice. + // Unfortunately, we cannot in general limit the size of the handler + // queue: we have to read every response that comes in on the wire + // (because it may be responding to a request issued by, say, an + // asynchronous handler), and in order to get to that response we have + // to read all of the requests that came in ahead of it. + s.handlerQueue = append(s.handlerQueue, req) + if !s.handlerRunning { + // We start the handleAsync goroutine when it has work to do, and let it + // exit when the queue empties. + // + // Otherwise, in order to synchronize the handler we would need some other + // goroutine (probably readIncoming?) to explicitly wait for handleAsync + // to finish, and that would complicate error reporting: either the error + // report from the goroutine would be blocked on the handler emptying its + // queue (which was tried, and introduced a deadlock detected by + // TestCloseCallRace), or the error would need to be reported separately + // from synchronizing completion. Allowing the handler goroutine to exit + // when idle seems simpler than trying to implement either of those + // alternatives correctly. + s.handlerRunning = true + go c.handleAsync() + } + }) + if err != nil { + c.processResult("acceptRequest", req, nil, err) + } } -func (c *Connection) deliverMessages(ctx context.Context, handler Handler, fromQueue <-chan *incoming) { - defer c.async.done() - for entry := range fromQueue { - // cancel any messages in the queue that we have a pending cancel for - var result interface{} - rerr := entry.handleCtx.Err() - if rerr == nil { - // only deliver if not already cancelled - result, rerr = handler.Handle(entry.handleCtx, entry.request) +// handleAsync invokes the handler on the requests in the handler queue +// sequentially until the queue is empty. +func (c *Connection) handleAsync() { + for { + var req *incomingRequest + c.updateInFlight(func(s *inFlightState) { + if len(s.handlerQueue) > 0 { + req, s.handlerQueue = s.handlerQueue[0], s.handlerQueue[1:] + } else { + s.handlerRunning = false + } + }) + if req == nil { + return } - switch { - case rerr == ErrNotHandled: - // message not handled, report it back to the caller as an error - c.reply(entry, nil, fmt.Errorf("%w: %q", ErrMethodNotFound, entry.request.Method)) - case rerr == ErrAsyncResponse: - // message handled but the response will come later - default: - c.reply(entry, result, rerr) + + // Only deliver to the Handler if not already canceled. + if err := req.ctx.Err(); err != nil { + c.updateInFlight(func(s *inFlightState) { + if s.writeErr != nil { + // Assume that req.ctx was canceled due to s.writeErr. + // TODO(#51365): use a Context API to plumb this through req.ctx. + err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr) + } + }) + c.processResult("handleAsync", req, nil, err) + continue } + + result, err := c.handler.Handle(req.ctx, req.Request) + c.processResult(c.handler, req, result, err) } } -// reply is used to reply to an incoming request that has just been handled -func (c *Connection) reply(entry *incoming, result interface{}, rerr error) { - if entry.request.IsCall() { - // we have a call finishing, remove it from the incoming map - pending := <-c.incomingBox - defer func() { c.incomingBox <- pending }() - delete(pending, entry.request.ID) +// processResult processes the result of a request and, if appropriate, sends a response. +func (c *Connection) processResult(from interface{}, req *incomingRequest, result interface{}, err error) error { + switch err { + case ErrAsyncResponse: + if !req.IsCall() { + return c.internalErrorf("%#v returned ErrAsyncResponse for a %q Request without an ID", from, req.Method) + } + return nil // This request is still in flight, so don't record the result yet. + case ErrNotHandled, ErrMethodNotFound: + // Add detail describing the unhandled method. + err = fmt.Errorf("%w: %q", ErrMethodNotFound, req.Method) } - if err := c.respond(entry, result, rerr); err != nil { - // no way to propagate this error - //TODO: should we do more than just log it? - event.Error(entry.baseCtx, "jsonrpc2 message delivery failed", err) + + if req.endSpan == nil { + return c.internalErrorf("%#v produced a duplicate %q Response", from, req.Method) } -} -// respond sends a response. -// This is the code shared between reply and SendResponse. -func (c *Connection) respond(entry *incoming, result interface{}, rerr error) error { - var err error - if entry.request.IsCall() { - // send the response - if result == nil && rerr == nil { - // call with no response, send an error anyway - rerr = fmt.Errorf("%w: %q produced no response", ErrInternal, entry.request.Method) + if result != nil && err != nil { + c.internalErrorf("%#v returned a non-nil result with a non-nil error for %s:\n%v\n%#v", from, req.Method, err, result) + result = nil // Discard the spurious result and respond with err. + } + + if req.IsCall() { + if result == nil && err == nil { + err = c.internalErrorf("%#v returned a nil result and nil error for a %q Request that requires a Response", from, req.Method) } - var response *Response - response, err = NewResponse(entry.request.ID, result, rerr) - if err == nil { - // we write the response with the base context, in case the message was cancelled - err = c.write(entry.baseCtx, response) + + response, respErr := NewResponse(req.ID, result, err) + + // The caller could theoretically reuse the request's ID as soon as we've + // sent the response, so ensure that it is removed from the incoming map + // before sending. + c.updateInFlight(func(s *inFlightState) { + delete(s.incomingByID, req.ID) + }) + if respErr == nil { + writeErr := c.write(notDone{req.ctx}, response) + if err == nil { + err = writeErr + } + } else { + err = c.internalErrorf("%#v returned a malformed result for %q: %w", from, req.Method, respErr) } - } else { - switch { - case rerr != nil: - // notification failed - err = fmt.Errorf("%w: %q notification failed: %v", ErrInternal, entry.request.Method, rerr) - rerr = nil - case result != nil: - //notification produced a response, which is an error - err = fmt.Errorf("%w: %q produced unwanted response", ErrInternal, entry.request.Method) - default: - // normal notification finish + } else { // req is a notification + if result != nil { + err = c.internalErrorf("%#v returned a non-nil result for a %q Request without an ID", from, req.Method) + } else if err != nil { + err = fmt.Errorf("%w: %q notification failed: %v", ErrInternal, req.Method, err) + } + if err != nil { + // TODO: can/should we do anything with this error beyond writing it to the event log? + // (Is this the right label to attach to the log?) + event.Label(req.ctx, keys.Err.Of(err)) } } - switch { - case rerr != nil || err != nil: - event.Label(entry.baseCtx, tag.StatusCode.Of("ERROR")) - default: - event.Label(entry.baseCtx, tag.StatusCode.Of("OK")) - } - // and just to be clean, invoke and clear the cancel if needed - if entry.cancel != nil { - entry.cancel() - entry.cancel = nil - } - // mark the entire request processing as done - entry.done() - return err + + labelStatus(req.ctx, err) + + // Cancel the request and finalize the event span to free any associated resources. + req.cancel() + req.endSpan() + req.endSpan = nil + c.updateInFlight(func(s *inFlightState) { + if s.incoming == 0 { + panic("jsonrpc2_v2: processResult called when incoming count is already zero") + } + s.incoming-- + }) + return nil } // write is used by all things that write outgoing messages, including replies. // it makes sure that writes are atomic func (c *Connection) write(ctx context.Context, msg Message) error { - writer := <-c.writerBox - defer func() { c.writerBox <- writer }() + writer := <-c.writer + defer func() { c.writer <- writer }() n, err := writer.Write(ctx, msg) event.Metric(ctx, tag.SentBytes.Of(n)) + + if err != nil && ctx.Err() == nil { + // The call to Write failed, and since ctx.Err() is nil we can't attribute + // the failure (even indirectly) to Context cancellation. The writer appears + // to be broken, and future writes are likely to also fail. + // + // If the read side of the connection is also broken, we might not even be + // able to receive cancellation notifications. Since we can't reliably write + // the results of incoming calls and can't receive explicit cancellations, + // cancel the calls now. + c.updateInFlight(func(s *inFlightState) { + if s.writeErr == nil { + s.writeErr = err + for _, r := range s.incomingByID { + r.cancel() + } + } + }) + } + return err } + +// internalErrorf reports an internal error. By default it panics, but if +// c.onInternalError is non-nil it instead calls that and returns an error +// wrapping ErrInternal. +func (c *Connection) internalErrorf(format string, args ...interface{}) error { + err := fmt.Errorf(format, args...) + if c.onInternalError == nil { + panic("jsonrpc2: " + err.Error()) + } + c.onInternalError(err) + + return fmt.Errorf("%w: %v", ErrInternal, err) +} + +// labelStatus labels the status of the event in ctx based on whether err is nil. +func labelStatus(ctx context.Context, err error) { + if err == nil { + event.Label(ctx, tag.StatusCode.Of("OK")) + } else { + event.Label(ctx, tag.StatusCode.Of("ERROR")) + } +} + +// notDone is a context.Context wrapper that returns a nil Done channel. +type notDone struct{ ctx context.Context } + +func (ic notDone) Value(key interface{}) interface{} { + return ic.ctx.Value(key) +} + +func (notDone) Done() <-chan struct{} { return nil } +func (notDone) Err() error { return nil } +func (notDone) Deadline() (time.Time, bool) { return time.Time{}, false } diff --git a/internal/jsonrpc2_v2/frame.go b/internal/jsonrpc2_v2/frame.go index b2b7dc1a172..e4248328132 100644 --- a/internal/jsonrpc2_v2/frame.go +++ b/internal/jsonrpc2_v2/frame.go @@ -120,6 +120,12 @@ func (r *headerReader) Read(ctx context.Context) (Message, int64, error) { line, err := r.in.ReadString('\n') total += int64(len(line)) if err != nil { + if err == io.EOF { + if total == 0 { + return nil, 0, io.EOF + } + err = io.ErrUnexpectedEOF + } return nil, total, fmt.Errorf("failed reading header line: %w", err) } line = strings.TrimSpace(line) diff --git a/internal/jsonrpc2_v2/jsonrpc2.go b/internal/jsonrpc2_v2/jsonrpc2.go index e685584427a..e9164b0bc95 100644 --- a/internal/jsonrpc2_v2/jsonrpc2.go +++ b/internal/jsonrpc2_v2/jsonrpc2.go @@ -47,6 +47,15 @@ type Preempter interface { Preempt(ctx context.Context, req *Request) (result interface{}, err error) } +// A PreempterFunc implements the Preempter interface for a standalone Preempt function. +type PreempterFunc func(ctx context.Context, req *Request) (interface{}, error) + +func (f PreempterFunc) Preempt(ctx context.Context, req *Request) (interface{}, error) { + return f(ctx, req) +} + +var _ Preempter = PreempterFunc(nil) + // Handler handles messages on a connection. type Handler interface { // Handle is invoked sequentially for each incoming request that has not @@ -75,12 +84,15 @@ func (defaultHandler) Handle(context.Context, *Request) (interface{}, error) { return nil, ErrNotHandled } +// A HandlerFunc implements the Handler interface for a standalone Handle function. type HandlerFunc func(ctx context.Context, req *Request) (interface{}, error) func (f HandlerFunc) Handle(ctx context.Context, req *Request) (interface{}, error) { return f(ctx, req) } +var _ Handler = HandlerFunc(nil) + // async is a small helper for operations with an asynchronous result that you // can wait for. type async struct { diff --git a/internal/jsonrpc2_v2/jsonrpc2_test.go b/internal/jsonrpc2_v2/jsonrpc2_test.go index 8e90c235f93..dd8d09c8870 100644 --- a/internal/jsonrpc2_v2/jsonrpc2_test.go +++ b/internal/jsonrpc2_v2/jsonrpc2_test.go @@ -11,7 +11,6 @@ import ( "path" "reflect" "testing" - "time" "golang.org/x/tools/internal/event/export/eventtest" jsonrpc2 "golang.org/x/tools/internal/jsonrpc2_v2" @@ -77,7 +76,7 @@ type binder struct { type handler struct { conn *jsonrpc2.Connection accumulator int - waitersBox chan map[string]chan struct{} + waiters chan map[string]chan struct{} calls map[string]*jsonrpc2.AsyncCall } @@ -137,10 +136,7 @@ func testConnection(t *testing.T, framer jsonrpc2.Framer) { if err != nil { t.Fatal(err) } - server, err := jsonrpc2.Serve(ctx, listener, binder{framer, nil}) - if err != nil { - t.Fatal(err) - } + server := jsonrpc2.NewServer(ctx, listener, binder{framer, nil}) defer func() { listener.Close() server.Wait() @@ -254,13 +250,13 @@ func verifyResults(t *testing.T, method string, results interface{}, expect inte } } -func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) (jsonrpc2.ConnectionOptions, error) { +func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { h := &handler{ - conn: conn, - waitersBox: make(chan map[string]chan struct{}, 1), - calls: make(map[string]*jsonrpc2.AsyncCall), + conn: conn, + waiters: make(chan map[string]chan struct{}, 1), + calls: make(map[string]*jsonrpc2.AsyncCall), } - h.waitersBox <- make(map[string]chan struct{}) + h.waiters <- make(map[string]chan struct{}) if b.runTest != nil { go b.runTest(h) } @@ -268,12 +264,12 @@ func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) (jsonrpc2.C Framer: b.framer, Preempter: h, Handler: h, - }, nil + } } func (h *handler) waiter(name string) chan struct{} { - waiters := <-h.waitersBox - defer func() { h.waitersBox <- waiters }() + waiters := <-h.waiters + defer func() { h.waiters <- waiters }() waiter, found := waiters[name] if !found { waiter = make(chan struct{}) @@ -370,8 +366,6 @@ func (h *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{ return true, nil case <-ctx.Done(): return nil, ctx.Err() - case <-time.After(time.Second): - return nil, fmt.Errorf("wait for %q timed out", name) } case "fork": var name string @@ -385,8 +379,6 @@ func (h *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{ h.conn.Respond(req.ID, true, nil) case <-ctx.Done(): h.conn.Respond(req.ID, nil, ctx.Err()) - case <-time.After(time.Second): - h.conn.Respond(req.ID, nil, fmt.Errorf("wait for %q timed out", name)) } }() return nil, jsonrpc2.ErrAsyncResponse diff --git a/internal/jsonrpc2_v2/net.go b/internal/jsonrpc2_v2/net.go index f1e2b0c7b36..15d0aea3af0 100644 --- a/internal/jsonrpc2_v2/net.go +++ b/internal/jsonrpc2_v2/net.go @@ -9,7 +9,6 @@ import ( "io" "net" "os" - "time" ) // This file contains implementations of the transport primitives that use the standard network @@ -36,7 +35,7 @@ type netListener struct { } // Accept blocks waiting for an incoming connection to the listener. -func (l *netListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { +func (l *netListener) Accept(context.Context) (io.ReadWriteCloser, error) { return l.net.Accept() } @@ -56,9 +55,7 @@ func (l *netListener) Close() error { // Dialer returns a dialer that can be used to connect to the listener. func (l *netListener) Dialer() Dialer { - return NetDialer(l.net.Addr().Network(), l.net.Addr().String(), net.Dialer{ - Timeout: 5 * time.Second, - }) + return NetDialer(l.net.Addr().Network(), l.net.Addr().String(), net.Dialer{}) } // NetDialer returns a Dialer using the supplied standard network dialer. @@ -98,15 +95,19 @@ type netPiper struct { } // Accept blocks waiting for an incoming connection to the listener. -func (l *netPiper) Accept(ctx context.Context) (io.ReadWriteCloser, error) { - // block until we have a listener, or are closed or cancelled +func (l *netPiper) Accept(context.Context) (io.ReadWriteCloser, error) { + // Block until the pipe is dialed or the listener is closed, + // preferring the latter if already closed at the start of Accept. + select { + case <-l.done: + return nil, errClosed + default: + } select { case rwc := <-l.dialed: return rwc, nil case <-l.done: - return nil, io.EOF - case <-ctx.Done(): - return nil, ctx.Err() + return nil, errClosed } } @@ -124,6 +125,14 @@ func (l *netPiper) Dialer() Dialer { func (l *netPiper) Dial(ctx context.Context) (io.ReadWriteCloser, error) { client, server := net.Pipe() - l.dialed <- server - return client, nil + + select { + case l.dialed <- server: + return client, nil + + case <-l.done: + client.Close() + server.Close() + return nil, errClosed + } } diff --git a/internal/jsonrpc2_v2/serve.go b/internal/jsonrpc2_v2/serve.go index 646267b5573..7df785655d7 100644 --- a/internal/jsonrpc2_v2/serve.go +++ b/internal/jsonrpc2_v2/serve.go @@ -6,12 +6,11 @@ package jsonrpc2 import ( "context" - "errors" + "fmt" "io" "runtime" - "strings" "sync" - "syscall" + "sync/atomic" "time" ) @@ -42,35 +41,43 @@ type Server struct { listener Listener binder Binder async *async + + shutdownOnce sync.Once + closing int32 // atomic: set to nonzero when Shutdown is called } // Dial uses the dialer to make a new connection, wraps the returned // reader and writer using the framer to make a stream, and then builds // a connection on top of that stream using the binder. +// +// The returned Connection will operate independently using the Preempter and/or +// Handler provided by the Binder, and will release its own resources when the +// connection is broken, but the caller may Close it earlier to stop accepting +// (or sending) new requests. func Dial(ctx context.Context, dialer Dialer, binder Binder) (*Connection, error) { // dial a server rwc, err := dialer.Dial(ctx) if err != nil { return nil, err } - return newConnection(ctx, rwc, binder) + return newConnection(ctx, rwc, binder, nil), nil } -// Serve starts a new server listening for incoming connections and returns +// NewServer starts a new server listening for incoming connections and returns // it. // This returns a fully running and connected server, it does not block on // the listener. // You can call Wait to block on the server, or Shutdown to get the sever to // terminate gracefully. // To notice incoming connections, use an intercepting Binder. -func Serve(ctx context.Context, listener Listener, binder Binder) (*Server, error) { +func NewServer(ctx context.Context, listener Listener, binder Binder) *Server { server := &Server{ listener: listener, binder: binder, async: newAsync(), } go server.run(ctx) - return server, nil + return server } // Wait returns only when the server has shut down. @@ -78,173 +85,160 @@ func (s *Server) Wait() error { return s.async.wait() } +// Shutdown informs the server to stop accepting new connections. +func (s *Server) Shutdown() { + s.shutdownOnce.Do(func() { + atomic.StoreInt32(&s.closing, 1) + s.listener.Close() + }) +} + // run accepts incoming connections from the listener, // If IdleTimeout is non-zero, run exits after there are no clients for this // duration, otherwise it exits only on error. func (s *Server) run(ctx context.Context) { defer s.async.done() - var activeConns []*Connection + + var activeConns sync.WaitGroup for { - // we never close the accepted connection, we rely on the other end - // closing or the socket closing itself naturally rwc, err := s.listener.Accept(ctx) if err != nil { - if !isClosingError(err) { + // Only Shutdown closes the listener. If we get an error after Shutdown is + // called, assume that that was the cause and don't report the error; + // otherwise, report the error in case it is unexpected. + if atomic.LoadInt32(&s.closing) == 0 { s.async.setError(err) } - // we are done generating new connections for good + // We are done generating new connections for good. break } - // see if any connections were closed while we were waiting - activeConns = onlyActive(activeConns) - - // a new inbound connection, - conn, err := newConnection(ctx, rwc, s.binder) - if err != nil { - if !isClosingError(err) { - s.async.setError(err) - } - continue - } - activeConns = append(activeConns, conn) - } - - // wait for all active conns to finish - for _, c := range activeConns { - c.Wait() + // A new inbound connection. + activeConns.Add(1) + _ = newConnection(ctx, rwc, s.binder, activeConns.Done) // unregisters itself when done } + activeConns.Wait() } -func onlyActive(conns []*Connection) []*Connection { - i := 0 - for _, c := range conns { - if !c.async.isDone() { - conns[i] = c - i++ - } +// NewIdleListener wraps a listener with an idle timeout. +// +// When there are no active connections for at least the timeout duration, +// calls to Accept will fail with ErrIdleTimeout. +// +// A connection is considered inactive as soon as its Close method is called. +func NewIdleListener(timeout time.Duration, wrap Listener) Listener { + l := &idleListener{ + wrapped: wrap, + timeout: timeout, + active: make(chan int, 1), + timedOut: make(chan struct{}), + idleTimer: make(chan *time.Timer, 1), } - // trim the slice down - return conns[:i] + l.idleTimer <- time.AfterFunc(l.timeout, l.timerExpired) + return l } -// isClosingError reports if the error occurs normally during the process of -// closing a network connection. It uses imperfect heuristics that err on the -// side of false negatives, and should not be used for anything critical. -func isClosingError(err error) bool { - if err == nil { - return false - } - // Fully unwrap the error, so the following tests work. - for wrapped := err; wrapped != nil; wrapped = errors.Unwrap(err) { - err = wrapped - } - - // Was it based on an EOF error? - if err == io.EOF { - return true - } +type idleListener struct { + wrapped Listener + timeout time.Duration - // Was it based on a closed pipe? - if err == io.ErrClosedPipe { - return true - } + // Only one of these channels is receivable at any given time. + active chan int // count of active connections; closed when Close is called if not timed out + timedOut chan struct{} // closed when the idle timer expires + idleTimer chan *time.Timer // holds the timer only when idle +} - // Per https://github.com/golang/go/issues/4373, this error string should not - // change. This is not ideal, but since the worst that could happen here is - // some superfluous logging, it is acceptable. - if err.Error() == "use of closed network connection" { - return true - } +// Accept accepts an incoming connection. +// +// If an incoming connection is accepted concurrent to the listener being closed +// due to idleness, the new connection is immediately closed. +func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { + rwc, err := l.wrapped.Accept(ctx) - if runtime.GOOS == "plan9" { - // Error reading from a closed connection. - if err == syscall.EINVAL { - return true + select { + case n, ok := <-l.active: + if err != nil { + if ok { + l.active <- n + } + return nil, err } - // Error trying to accept a new connection from a closed listener. - if strings.HasSuffix(err.Error(), " listen hungup") { - return true + if ok { + l.active <- n + 1 + } else { + // l.wrapped.Close Close has been called, but Accept returned a + // connection. This race can occur with concurrent Accept and Close calls + // with any net.Listener, and it is benign: since the listener was closed + // explicitly, it can't have also timed out. } - } - return false -} + return l.newConn(rwc), nil -// NewIdleListener wraps a listener with an idle timeout. -// When there are no active connections for at least the timeout duration a -// call to accept will fail with ErrIdleTimeout. -func NewIdleListener(timeout time.Duration, wrap Listener) Listener { - l := &idleListener{ - timeout: timeout, - wrapped: wrap, - newConns: make(chan *idleCloser), - closed: make(chan struct{}), - wasTimeout: make(chan struct{}), - } - go l.run() - return l -} + case <-l.timedOut: + if err == nil { + // Keeping the connection open would leave the listener simultaneously + // active and closed due to idleness, which would be contradictory and + // confusing. Close the connection and pretend that it never happened. + rwc.Close() + } else { + // In theory the timeout could have raced with an unrelated error return + // from Accept. However, ErrIdleTimeout is arguably still valid (since we + // would have closed due to the timeout independent of the error), and the + // harm from returning a spurious ErrIdleTimeout is negliglible anyway. + } + return nil, ErrIdleTimeout -type idleListener struct { - wrapped Listener - timeout time.Duration - newConns chan *idleCloser - closed chan struct{} - wasTimeout chan struct{} - closeOnce sync.Once -} + case timer := <-l.idleTimer: + if err != nil { + // The idle timer doesn't run until it receives itself from the idleTimer + // channel, so it can't have called l.wrapped.Close yet and thus err can't + // be ErrIdleTimeout. Leave the idle timer as it was and return whatever + // error we got. + l.idleTimer <- timer + return nil, err + } -type idleCloser struct { - wrapped io.ReadWriteCloser - closed chan struct{} - closeOnce sync.Once -} + if !timer.Stop() { + // Failed to stop the timer — the timer goroutine is in the process of + // firing. Send the timer back to the timer goroutine so that it can + // safely close the timedOut channel, and then wait for the listener to + // actually be closed before we return ErrIdleTimeout. + l.idleTimer <- timer + rwc.Close() + <-l.timedOut + return nil, ErrIdleTimeout + } -func (c *idleCloser) Read(p []byte) (int, error) { - n, err := c.wrapped.Read(p) - if err != nil && isClosingError(err) { - c.closeOnce.Do(func() { close(c.closed) }) + l.active <- 1 + return l.newConn(rwc), nil } - return n, err } -func (c *idleCloser) Write(p []byte) (int, error) { - // we do not close on write failure, we rely on the wrapped writer to do that - // if it is appropriate, which we will detect in the next read. - return c.wrapped.Write(p) -} +func (l *idleListener) Close() error { + select { + case _, ok := <-l.active: + if ok { + close(l.active) + } -func (c *idleCloser) Close() error { - // we rely on closing the wrapped stream to signal to the next read that we - // are closed, rather than triggering the closed signal directly - return c.wrapped.Close() -} + case <-l.timedOut: + // Already closed by the timer; take care not to double-close if the caller + // only explicitly invokes this Close method once, since the io.Closer + // interface explicitly leaves doubled Close calls undefined. + return ErrIdleTimeout -func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) { - rwc, err := l.wrapped.Accept(ctx) - if err != nil { - if isClosingError(err) { - // underlying listener was closed - l.closeOnce.Do(func() { close(l.closed) }) - // was it closed because of the idle timeout? - select { - case <-l.wasTimeout: - err = ErrIdleTimeout - default: - } + case timer := <-l.idleTimer: + if !timer.Stop() { + // Couldn't stop the timer. It shouldn't take long to run, so just wait + // (so that the Listener is guaranteed to be closed before we return) + // and pretend that this call happened afterward. + // That way we won't leak any timers or goroutines when Close returns. + l.idleTimer <- timer + <-l.timedOut + return ErrIdleTimeout } - return nil, err + close(l.active) } - conn := &idleCloser{ - wrapped: rwc, - closed: make(chan struct{}), - } - l.newConns <- conn - return conn, err -} -func (l *idleListener) Close() error { - defer l.closeOnce.Do(func() { close(l.closed) }) return l.wrapped.Close() } @@ -252,31 +246,83 @@ func (l *idleListener) Dialer() Dialer { return l.wrapped.Dialer() } -func (l *idleListener) run() { - var conns []*idleCloser - for { - var firstClosed chan struct{} // left at nil if there are no active conns - var timeout <-chan time.Time // left at nil if there are active conns - if len(conns) > 0 { - firstClosed = conns[0].closed +func (l *idleListener) timerExpired() { + select { + case n, ok := <-l.active: + if ok { + panic(fmt.Sprintf("jsonrpc2: idleListener idle timer fired with %d connections still active", n)) } else { - timeout = time.After(l.timeout) + panic("jsonrpc2: Close finished with idle timer still running") } - select { - case <-l.closed: - // the main listener closed, no need to keep going + + case <-l.timedOut: + panic("jsonrpc2: idleListener idle timer fired more than once") + + case <-l.idleTimer: + // The timer for this very call! + } + + // Close the Listener with all channels still blocked to ensure that this call + // to l.wrapped.Close doesn't race with the one in l.Close. + defer close(l.timedOut) + l.wrapped.Close() +} + +func (l *idleListener) connClosed() { + select { + case n, ok := <-l.active: + if !ok { + // l is already closed, so it can't close due to idleness, + // and we don't need to track the number of active connections any more. return - case conn := <-l.newConns: - // a new conn arrived, add it to the list - conns = append(conns, conn) - case <-timeout: - // we timed out, only happens when there are no active conns - // close the underlying listener, and allow the normal closing process to happen - close(l.wasTimeout) - l.wrapped.Close() - case <-firstClosed: - // a conn closed, remove it from the active list - conns = conns[:copy(conns, conns[1:])] } + n-- + if n == 0 { + l.idleTimer <- time.AfterFunc(l.timeout, l.timerExpired) + } else { + l.active <- n + } + + case <-l.timedOut: + panic("jsonrpc2: idleListener idle timer fired before last active connection was closed") + + case <-l.idleTimer: + panic("jsonrpc2: idleListener idle timer active before last active connection was closed") } } + +type idleListenerConn struct { + wrapped io.ReadWriteCloser + l *idleListener + closeOnce sync.Once +} + +func (l *idleListener) newConn(rwc io.ReadWriteCloser) *idleListenerConn { + c := &idleListenerConn{ + wrapped: rwc, + l: l, + } + + // A caller that forgets to call Close may disrupt the idleListener's + // accounting, even though the file descriptor for the underlying connection + // may eventually be garbage-collected anyway. + // + // Set a (best-effort) finalizer to verify that a Close call always occurs. + // (We will clear the finalizer explicitly in Close.) + runtime.SetFinalizer(c, func(c *idleListenerConn) { + panic("jsonrpc2: IdleListener connection became unreachable without a call to Close") + }) + + return c +} + +func (c *idleListenerConn) Read(p []byte) (int, error) { return c.wrapped.Read(p) } +func (c *idleListenerConn) Write(p []byte) (int, error) { return c.wrapped.Write(p) } + +func (c *idleListenerConn) Close() error { + defer c.closeOnce.Do(func() { + c.l.connClosed() + runtime.SetFinalizer(c, nil) + }) + return c.wrapped.Close() +} diff --git a/internal/jsonrpc2_v2/serve_go116.go b/internal/jsonrpc2_v2/serve_go116.go new file mode 100644 index 00000000000..29549f1059d --- /dev/null +++ b/internal/jsonrpc2_v2/serve_go116.go @@ -0,0 +1,19 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.16 +// +build go1.16 + +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 new file mode 100644 index 00000000000..14afa834962 --- /dev/null +++ b/internal/jsonrpc2_v2/serve_pre116.go @@ -0,0 +1,30 @@ +// 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. + +//go:build !go1.16 +// +build !go1.16 + +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 compatiblity with + // (otherwise-unsupported) older Go versions. + // + // In the meantime, this error strirng 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/jsonrpc2_v2/serve_test.go b/internal/jsonrpc2_v2/serve_test.go index 26cf6a58c4e..88ac66b7e66 100644 --- a/internal/jsonrpc2_v2/serve_test.go +++ b/internal/jsonrpc2_v2/serve_test.go @@ -7,6 +7,8 @@ package jsonrpc2_test import ( "context" "errors" + "fmt" + "runtime/debug" "testing" "time" @@ -16,48 +18,125 @@ import ( func TestIdleTimeout(t *testing.T) { stacktest.NoLeak(t) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - listener, err := jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{}) - if err != nil { - t.Fatal(err) - } - listener = jsonrpc2.NewIdleListener(100*time.Millisecond, listener) - defer listener.Close() - server, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.ConnectionOptions{}) - if err != nil { - t.Fatal(err) - } + // Use a panicking time.AfterFunc instead of context.WithTimeout so that we + // get a goroutine dump on failure. We expect the test to take on the order of + // a few tens of milliseconds at most, so 10s should be several orders of + // magnitude of headroom. + timer := time.AfterFunc(10*time.Second, func() { + debug.SetTraceback("all") + panic("TestIdleTimeout deadlocked") + }) + defer timer.Stop() - connect := func() *jsonrpc2.Connection { - client, err := jsonrpc2.Dial(ctx, - listener.Dialer(), - jsonrpc2.ConnectionOptions{}) + ctx := context.Background() + + try := func(d time.Duration) (longEnough bool) { + listener, err := jsonrpc2.NetListener(ctx, "tcp", "localhost:0", jsonrpc2.NetListenOptions{}) if err != nil { t.Fatal(err) } - return client - } - // Exercise some connection/disconnection patterns, and then assert that when - // our timer fires, the server exits. - conn1 := connect() - conn2 := connect() - if err := conn1.Close(); err != nil { - t.Fatalf("conn1.Close failed with error: %v", err) - } - if err := conn2.Close(); err != nil { - t.Fatalf("conn2.Close failed with error: %v", err) - } - conn3 := connect() - if err := conn3.Close(); err != nil { - t.Fatalf("conn3.Close failed with error: %v", err) - } - serverError := server.Wait() + idleStart := time.Now() + listener = jsonrpc2.NewIdleListener(d, listener) + defer listener.Close() - if !errors.Is(serverError, jsonrpc2.ErrIdleTimeout) { - t.Errorf("run() returned error %v, want %v", serverError, jsonrpc2.ErrIdleTimeout) + server := jsonrpc2.NewServer(ctx, listener, jsonrpc2.ConnectionOptions{}) + + // Exercise some connection/disconnection patterns, and then assert that when + // our timer fires, the server exits. + conn1, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + if err != nil { + if since := time.Since(idleStart); since < d { + t.Fatalf("conn1 failed to connect after %v: %v", since, err) + } + t.Log("jsonrpc2.Dial:", err) + return false // Took to long to dial, so the failure could have been due to the idle timeout. + } + // On the server side, Accept can race with the connection timing out. + // Send a call and wait for the response to ensure that the connection was + // actually fully accepted. + ac := conn1.Call(ctx, "ping", nil) + if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { + if since := time.Since(idleStart); since < d { + t.Fatalf("conn1 broken after %v: %v", since, err) + } + t.Log(`conn1.Call(ctx, "ping", nil):`, err) + conn1.Close() + return false + } + + // Since conn1 was successfully accepted and remains open, the server is + // definitely non-idle. Dialing another simultaneous connection should + // succeed. + conn2, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + if err != nil { + conn1.Close() + t.Fatalf("conn2 failed to connect while non-idle after %v: %v", time.Since(idleStart), err) + return false + } + // Ensure that conn2 is also accepted on the server side before we close + // conn1. Otherwise, the connection can appear idle if the server processes + // the closure of conn1 and the idle timeout before it finally notices conn2 + // in the accept queue. + // (That failure mode may explain the failure noted in + // https://go.dev/issue/49387#issuecomment-1303979877.) + ac = conn2.Call(ctx, "ping", nil) + if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { + t.Fatalf("conn2 broken while non-idle after %v: %v", time.Since(idleStart), err) + } + + if err := conn1.Close(); err != nil { + t.Fatalf("conn1.Close failed with error: %v", err) + } + idleStart = time.Now() + if err := conn2.Close(); err != nil { + t.Fatalf("conn2.Close failed with error: %v", err) + } + + conn3, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + if err != nil { + if since := time.Since(idleStart); since < d { + t.Fatalf("conn3 failed to connect after %v: %v", since, err) + } + t.Log("jsonrpc2.Dial:", err) + return false // Took to long to dial, so the failure could have been due to the idle timeout. + } + + ac = conn3.Call(ctx, "ping", nil) + if err := ac.Await(ctx, nil); !errors.Is(err, jsonrpc2.ErrMethodNotFound) { + if since := time.Since(idleStart); since < d { + t.Fatalf("conn3 broken after %v: %v", since, err) + } + t.Log(`conn3.Call(ctx, "ping", nil):`, err) + conn3.Close() + return false + } + + idleStart = time.Now() + if err := conn3.Close(); err != nil { + t.Fatalf("conn3.Close failed with error: %v", err) + } + + serverError := server.Wait() + + if !errors.Is(serverError, jsonrpc2.ErrIdleTimeout) { + t.Errorf("run() returned error %v, want %v", serverError, jsonrpc2.ErrIdleTimeout) + } + if since := time.Since(idleStart); since < d { + t.Errorf("server shut down after %v idle; want at least %v", since, d) + } + return true + } + + d := 1 * time.Millisecond + for { + t.Logf("testing with idle timout %v", d) + if !try(d) { + d *= 2 + continue + } + break } } @@ -78,8 +157,7 @@ func (fakeHandler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface func TestServe(t *testing.T) { stacktest.NoLeak(t) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + ctx := context.Background() tests := []struct { name string @@ -116,13 +194,9 @@ func TestServe(t *testing.T) { } func newFake(t *testing.T, ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2.Connection, func(), error) { - l = jsonrpc2.NewIdleListener(100*time.Millisecond, l) - server, err := jsonrpc2.Serve(ctx, l, jsonrpc2.ConnectionOptions{ + server := jsonrpc2.NewServer(ctx, l, jsonrpc2.ConnectionOptions{ Handler: fakeHandler{}, }) - if err != nil { - return nil, nil, err - } client, err := jsonrpc2.Dial(ctx, l.Dialer(), @@ -142,3 +216,129 @@ func newFake(t *testing.T, ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2. server.Wait() }, nil } + +// TestIdleListenerAcceptCloseRace checks for the Accept/Close race fixed in CL 388597. +// +// (A bug in the idleListener implementation caused a successful Accept to block +// on sending to a background goroutine that could have already exited.) +func TestIdleListenerAcceptCloseRace(t *testing.T) { + ctx := context.Background() + + n := 10 + + // Each iteration of the loop appears to take around a millisecond, so to + // avoid spurious failures we'll set the watchdog for three orders of + // magnitude longer. When the bug was present, this reproduced the deadlock + // reliably on a Linux workstation when run with -count=100, which should be + // frequent enough to show up on the Go build dashboard if it regresses. + watchdog := time.Duration(n) * 1000 * time.Millisecond + timer := time.AfterFunc(watchdog, func() { + debug.SetTraceback("all") + panic(fmt.Sprintf("%s deadlocked after %v", t.Name(), watchdog)) + }) + defer timer.Stop() + + for ; n > 0; n-- { + listener, err := jsonrpc2.NetPipeListener(ctx) + if err != nil { + t.Fatal(err) + } + listener = jsonrpc2.NewIdleListener(24*time.Hour, listener) + + done := make(chan struct{}) + go func() { + conn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + listener.Close() + if err == nil { + conn.Close() + } + close(done) + }() + + // Accept may return a non-nil error if Close closes the underlying network + // connection before the wrapped Accept call unblocks. However, it must not + // deadlock! + c, err := listener.Accept(ctx) + if err == nil { + c.Close() + } + <-done + } +} + +// TestCloseCallRace checks for a race resulting in a deadlock when a Call on +// one side of the connection races with a Close (or otherwise broken +// connection) initiated from the other side. +// +// (The Call method was waiting for a result from the Read goroutine to +// determine which error value to return, but the Read goroutine was waiting for +// in-flight calls to complete before reporting that result.) +func TestCloseCallRace(t *testing.T) { + ctx := context.Background() + n := 10 + + watchdog := time.Duration(n) * 1000 * time.Millisecond + timer := time.AfterFunc(watchdog, func() { + debug.SetTraceback("all") + panic(fmt.Sprintf("%s deadlocked after %v", t.Name(), watchdog)) + }) + defer timer.Stop() + + for ; n > 0; n-- { + listener, err := jsonrpc2.NetPipeListener(ctx) + if err != nil { + t.Fatal(err) + } + + pokec := make(chan *jsonrpc2.AsyncCall, 1) + + s := jsonrpc2.NewServer(ctx, listener, jsonrpc2.BinderFunc(func(_ context.Context, srvConn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions { + h := jsonrpc2.HandlerFunc(func(ctx context.Context, _ *jsonrpc2.Request) (interface{}, error) { + // Start a concurrent call from the server to the client. + // The point of this test is to ensure this doesn't deadlock + // if the client shuts down the connection concurrently. + // + // The racing Call may or may not receive a response: it should get a + // response if it is sent before the client closes the connection, and + // it should fail with some kind of "connection closed" error otherwise. + go func() { + pokec <- srvConn.Call(ctx, "poke", nil) + }() + + return &msg{"pong"}, nil + }) + return jsonrpc2.ConnectionOptions{Handler: h} + })) + + dialConn, err := jsonrpc2.Dial(ctx, listener.Dialer(), jsonrpc2.ConnectionOptions{}) + if err != nil { + listener.Close() + s.Wait() + t.Fatal(err) + } + + // Calling any method on the server should provoke it to asynchronously call + // us back. While it is starting that call, we will close the connection. + if err := dialConn.Call(ctx, "ping", nil).Await(ctx, nil); err != nil { + t.Error(err) + } + if err := dialConn.Close(); err != nil { + t.Error(err) + } + + // Ensure that the Call on the server side did not block forever when the + // connection closed. + pokeCall := <-pokec + if err := pokeCall.Await(ctx, nil); err == nil { + t.Errorf("unexpected nil error from server-initited call") + } else if errors.Is(err, jsonrpc2.ErrMethodNotFound) { + // The call completed before the Close reached the handler. + } else { + // The error was something else. + t.Logf("server-initiated call completed with expected error: %v", err) + } + + listener.Close() + s.Wait() + } +} diff --git a/internal/jsonrpc2_v2/wire.go b/internal/jsonrpc2_v2/wire.go index 4da129ae6e2..c8dc9ebf1bf 100644 --- a/internal/jsonrpc2_v2/wire.go +++ b/internal/jsonrpc2_v2/wire.go @@ -33,6 +33,10 @@ var ( ErrServerOverloaded = NewError(-32000, "JSON RPC overloaded") // ErrUnknown should be used for all non coded errors. ErrUnknown = NewError(-32001, "JSON RPC unknown error") + // ErrServerClosing is returned for calls that arrive while the server is closing. + ErrServerClosing = NewError(-32002, "JSON RPC server is closing") + // ErrClientClosing is a dummy error returned for calls initiated while the client is closing. + ErrClientClosing = NewError(-32003, "JSON RPC client is closing") ) const wireVersion = "2.0" @@ -72,3 +76,11 @@ func NewError(code int64, message string) error { func (err *wireError) Error() string { return err.Message } + +func (err *wireError) Is(other error) bool { + w, ok := other.(*wireError) + if !ok { + return false + } + return err.Code == w.Code +} diff --git a/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go deleted file mode 100644 index 90290613d87..00000000000 --- a/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go +++ /dev/null @@ -1,41 +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 fillstruct - -type emptyStruct[A any] struct{} - -var _ = emptyStruct[int]{} - -type basicStruct[T any] struct { - foo T -} - -var _ = basicStruct[int]{} - -type fooType[T any] T - -type twoArgStruct[F, B any] struct { - foo fooType[F] - bar fooType[B] -} - -var _ = twoArgStruct[string, int]{} - -var _ = twoArgStruct[int, string]{ - bar: "bar", -} - -type nestedStruct struct { - bar string - basic basicStruct[int] -} - -var _ = nestedStruct{} - -func _[T any]() { - type S struct{ t T } - x := S{} - _ = x -} diff --git a/internal/lsp/cache/analysis.go b/internal/lsp/cache/analysis.go deleted file mode 100644 index e882fb46f07..00000000000 --- a/internal/lsp/cache/analysis.go +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - -import ( - "context" - "fmt" - "go/ast" - "go/types" - "reflect" - "sort" - "sync" - - "golang.org/x/sync/errgroup" - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/internal/analysisinternal" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/span" -) - -func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) { - var roots []*actionHandle - for _, a := range analyzers { - if !a.IsEnabled(s.view) { - continue - } - ah, err := s.actionHandle(ctx, PackageID(id), a.Analyzer) - if err != nil { - return nil, err - } - roots = append(roots, ah) - } - - // Check if the context has been canceled before running the analyses. - if ctx.Err() != nil { - return nil, ctx.Err() - } - - var results []*source.Diagnostic - for _, ah := range roots { - diagnostics, _, err := ah.analyze(ctx, s) - if err != nil { - // Keep going if a single analyzer failed. - event.Error(ctx, fmt.Sprintf("analyzer %q failed", ah.analyzer.Name), err) - continue - } - results = append(results, diagnostics...) - } - return results, nil -} - -type actionHandleKey string - -// An action represents one unit of analysis work: the application of -// one analysis to one package. Actions form a DAG, both within a -// package (as different analyzers are applied, either in sequence or -// parallel), and across packages (as dependencies are analyzed). -type actionHandle struct { - handle *memoize.Handle - - analyzer *analysis.Analyzer - pkg *pkg -} - -type actionData struct { - diagnostics []*source.Diagnostic - result interface{} - objectFacts map[objectFactKey]analysis.Fact - packageFacts map[packageFactKey]analysis.Fact - err error -} - -type objectFactKey struct { - obj types.Object - typ reflect.Type -} - -type packageFactKey struct { - pkg *types.Package - typ reflect.Type -} - -func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.Analyzer) (*actionHandle, error) { - ph, err := s.buildPackageHandle(ctx, id, source.ParseFull) - if err != nil { - return nil, err - } - act := s.getActionHandle(id, ph.mode, a) - if act != nil { - return act, nil - } - if len(ph.key) == 0 { - return nil, fmt.Errorf("actionHandle: no key for package %s", id) - } - pkg, err := ph.check(ctx, s) - if err != nil { - return nil, err - } - act = &actionHandle{ - analyzer: a, - pkg: pkg, - } - var deps []*actionHandle - // Add a dependency on each required analyzers. - for _, req := range a.Requires { - reqActionHandle, err := s.actionHandle(ctx, id, req) - if err != nil { - return nil, err - } - deps = append(deps, reqActionHandle) - } - - // TODO(golang/go#35089): Re-enable this when we doesn't use ParseExported - // mode for dependencies. In the meantime, disable analysis for dependencies, - // since we don't get anything useful out of it. - if false { - // An analysis that consumes/produces facts - // must run on the package's dependencies too. - if len(a.FactTypes) > 0 { - importIDs := make([]string, 0, len(ph.m.Deps)) - for _, importID := range ph.m.Deps { - importIDs = append(importIDs, string(importID)) - } - sort.Strings(importIDs) // for determinism - for _, importID := range importIDs { - depActionHandle, err := s.actionHandle(ctx, PackageID(importID), a) - if err != nil { - return nil, err - } - deps = append(deps, depActionHandle) - } - } - } - - h := s.generation.Bind(buildActionKey(a, ph), func(ctx context.Context, arg memoize.Arg) interface{} { - snapshot := arg.(*snapshot) - // Analyze dependencies first. - results, err := execAll(ctx, snapshot, deps) - if err != nil { - return &actionData{ - err: err, - } - } - return runAnalysis(ctx, snapshot, a, pkg, results) - }, nil) - act.handle = h - - act = s.addActionHandle(act) - return act, nil -} - -func (act *actionHandle) analyze(ctx context.Context, snapshot *snapshot) ([]*source.Diagnostic, interface{}, error) { - d, err := act.handle.Get(ctx, snapshot.generation, snapshot) - if err != nil { - return nil, nil, err - } - data, ok := d.(*actionData) - if !ok { - return nil, nil, fmt.Errorf("unexpected type for %s:%s", act.pkg.ID(), act.analyzer.Name) - } - if data == nil { - return nil, nil, fmt.Errorf("unexpected nil analysis for %s:%s", act.pkg.ID(), act.analyzer.Name) - } - return data.diagnostics, data.result, data.err -} - -func buildActionKey(a *analysis.Analyzer, ph *packageHandle) actionHandleKey { - return actionHandleKey(hashContents([]byte(fmt.Sprintf("%p %s", a, string(ph.key))))) -} - -func (act *actionHandle) String() string { - return fmt.Sprintf("%s@%s", act.analyzer, act.pkg.PkgPath()) -} - -func execAll(ctx context.Context, snapshot *snapshot, actions []*actionHandle) (map[*actionHandle]*actionData, error) { - var mu sync.Mutex - results := make(map[*actionHandle]*actionData) - - g, ctx := errgroup.WithContext(ctx) - for _, act := range actions { - act := act - g.Go(func() error { - v, err := act.handle.Get(ctx, snapshot.generation, snapshot) - if err != nil { - return err - } - data, ok := v.(*actionData) - if !ok { - return fmt.Errorf("unexpected type for %s: %T", act, v) - } - - mu.Lock() - defer mu.Unlock() - results[act] = data - - return nil - }) - } - return results, g.Wait() -} - -func runAnalysis(ctx context.Context, snapshot *snapshot, analyzer *analysis.Analyzer, pkg *pkg, deps map[*actionHandle]*actionData) (data *actionData) { - data = &actionData{ - objectFacts: make(map[objectFactKey]analysis.Fact), - packageFacts: make(map[packageFactKey]analysis.Fact), - } - defer func() { - if r := recover(); r != nil { - data.err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pkg.PkgPath(), r) - } - }() - - // Plumb the output values of the dependencies - // into the inputs of this action. Also facts. - inputs := make(map[*analysis.Analyzer]interface{}) - - for depHandle, depData := range deps { - if depHandle.pkg == pkg { - // Same package, different analysis (horizontal edge): - // in-memory outputs of prerequisite analyzers - // become inputs to this analysis pass. - inputs[depHandle.analyzer] = depData.result - } else if depHandle.analyzer == analyzer { // (always true) - // Same analysis, different package (vertical edge): - // serialized facts produced by prerequisite analysis - // become available to this analysis pass. - for key, fact := range depData.objectFacts { - // Filter out facts related to objects - // that are irrelevant downstream - // (equivalently: not in the compiler export data). - if !exportedFrom(key.obj, depHandle.pkg.types) { - continue - } - data.objectFacts[key] = fact - } - for key, fact := range depData.packageFacts { - // TODO: filter out facts that belong to - // packages not mentioned in the export data - // to prevent side channels. - - data.packageFacts[key] = fact - } - } - } - - var syntax []*ast.File - for _, cgf := range pkg.compiledGoFiles { - syntax = append(syntax, cgf.File) - } - - var diagnostics []*analysis.Diagnostic - - // Run the analysis. - pass := &analysis.Pass{ - Analyzer: analyzer, - Fset: snapshot.FileSet(), - Files: syntax, - Pkg: pkg.GetTypes(), - TypesInfo: pkg.GetTypesInfo(), - TypesSizes: pkg.GetTypesSizes(), - ResultOf: inputs, - Report: func(d analysis.Diagnostic) { - // Prefix the diagnostic category with the analyzer's name. - if d.Category == "" { - d.Category = analyzer.Name - } else { - d.Category = analyzer.Name + "." + d.Category - } - diagnostics = append(diagnostics, &d) - }, - ImportObjectFact: func(obj types.Object, ptr analysis.Fact) bool { - if obj == nil { - panic("nil object") - } - key := objectFactKey{obj, factType(ptr)} - - if v, ok := data.objectFacts[key]; ok { - reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem()) - return true - } - return false - }, - ExportObjectFact: func(obj types.Object, fact analysis.Fact) { - if obj.Pkg() != pkg.types { - panic(fmt.Sprintf("internal error: in analysis %s of package %s: Fact.Set(%s, %T): can't set facts on objects belonging another package", - analyzer, pkg.ID(), obj, fact)) - } - key := objectFactKey{obj, factType(fact)} - data.objectFacts[key] = fact // clobber any existing entry - }, - ImportPackageFact: func(pkg *types.Package, ptr analysis.Fact) bool { - if pkg == nil { - panic("nil package") - } - key := packageFactKey{pkg, factType(ptr)} - if v, ok := data.packageFacts[key]; ok { - reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem()) - return true - } - return false - }, - ExportPackageFact: func(fact analysis.Fact) { - key := packageFactKey{pkg.types, factType(fact)} - data.packageFacts[key] = fact // clobber any existing entry - }, - AllObjectFacts: func() []analysis.ObjectFact { - facts := make([]analysis.ObjectFact, 0, len(data.objectFacts)) - for k := range data.objectFacts { - facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: data.objectFacts[k]}) - } - return facts - }, - AllPackageFacts: func() []analysis.PackageFact { - facts := make([]analysis.PackageFact, 0, len(data.packageFacts)) - for k := range data.packageFacts { - facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: data.packageFacts[k]}) - } - return facts - }, - } - analysisinternal.SetTypeErrors(pass, pkg.typeErrors) - - if pkg.IsIllTyped() { - data.err = fmt.Errorf("analysis skipped due to errors in package") - return data - } - data.result, data.err = pass.Analyzer.Run(pass) - if data.err != nil { - return data - } - - if got, want := reflect.TypeOf(data.result), pass.Analyzer.ResultType; got != want { - data.err = fmt.Errorf( - "internal error: on package %s, analyzer %s returned a result of type %v, but declared ResultType %v", - pass.Pkg.Path(), pass.Analyzer, got, want) - return data - } - - // disallow calls after Run - pass.ExportObjectFact = func(obj types.Object, fact analysis.Fact) { - panic(fmt.Sprintf("%s:%s: Pass.ExportObjectFact(%s, %T) called after Run", analyzer.Name, pkg.PkgPath(), obj, fact)) - } - pass.ExportPackageFact = func(fact analysis.Fact) { - panic(fmt.Sprintf("%s:%s: Pass.ExportPackageFact(%T) called after Run", analyzer.Name, pkg.PkgPath(), fact)) - } - - for _, diag := range diagnostics { - srcDiags, err := analysisDiagnosticDiagnostics(snapshot, pkg, analyzer, diag) - if err != nil { - event.Error(ctx, "unable to compute analysis error position", err, tag.Category.Of(diag.Category), tag.Package.Of(pkg.ID())) - continue - } - if ctx.Err() != nil { - data.err = ctx.Err() - return data - } - data.diagnostics = append(data.diagnostics, srcDiags...) - } - return data -} - -// exportedFrom reports whether obj may be visible to a package that imports pkg. -// This includes not just the exported members of pkg, but also unexported -// constants, types, fields, and methods, perhaps belonging to other packages, -// that find there way into the API. -// This is an overapproximation of the more accurate approach used by -// gc export data, which walks the type graph, but it's much simpler. -// -// TODO(adonovan): do more accurate filtering by walking the type graph. -func exportedFrom(obj types.Object, pkg *types.Package) bool { - switch obj := obj.(type) { - case *types.Func: - return obj.Exported() && obj.Pkg() == pkg || - obj.Type().(*types.Signature).Recv() != nil - case *types.Var: - return obj.Exported() && obj.Pkg() == pkg || - obj.IsField() - case *types.TypeName, *types.Const: - return true - } - return false // Nil, Builtin, Label, or PkgName -} - -func factType(fact analysis.Fact) reflect.Type { - t := reflect.TypeOf(fact) - if t.Kind() != reflect.Ptr { - panic(fmt.Sprintf("invalid Fact type: got %T, want pointer", fact)) - } - return t -} - -func (s *snapshot) DiagnosePackage(ctx context.Context, spkg source.Package) (map[span.URI][]*source.Diagnostic, error) { - pkg := spkg.(*pkg) - // Apply type error analyzers. They augment type error diagnostics with their own fixes. - var analyzers []*source.Analyzer - for _, a := range s.View().Options().TypeErrorAnalyzers { - analyzers = append(analyzers, a) - } - var errorAnalyzerDiag []*source.Diagnostic - if pkg.HasTypeErrors() { - var err error - errorAnalyzerDiag, err = s.Analyze(ctx, pkg.ID(), analyzers) - if err != nil { - // Keep going: analysis failures should not block diagnostics. - event.Error(ctx, "type error analysis failed", err, tag.Package.Of(pkg.ID())) - } - } - diags := map[span.URI][]*source.Diagnostic{} - for _, diag := range pkg.diagnostics { - for _, eaDiag := range errorAnalyzerDiag { - if eaDiag.URI == diag.URI && eaDiag.Range == diag.Range && eaDiag.Message == diag.Message { - // Type error analyzers just add fixes and tags. Make a copy, - // since we don't own either, and overwrite. - // The analyzer itself can't do this merge because - // analysis.Diagnostic doesn't have all the fields, and Analyze - // can't because it doesn't have the type error, notably its code. - clone := *diag - clone.SuggestedFixes = eaDiag.SuggestedFixes - clone.Tags = eaDiag.Tags - clone.Analyzer = eaDiag.Analyzer - diag = &clone - } - } - diags[diag.URI] = append(diags[diag.URI], diag) - } - return diags, nil -} diff --git a/internal/lsp/cache/graph.go b/internal/lsp/cache/graph.go deleted file mode 100644 index f0f8724d375..00000000000 --- a/internal/lsp/cache/graph.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - -import "golang.org/x/tools/internal/span" - -// A metadataGraph holds information about a transtively closed import graph of -// Go packages, as obtained from go/packages. -// -// Currently a new metadata graph is created for each snapshot. -// TODO(rfindley): make this type immutable, so that it may be shared across -// snapshots. -type metadataGraph struct { - // ids maps file URIs to package IDs. A single file may belong to multiple - // packages due to tests packages. - ids map[span.URI][]PackageID - - // metadata maps package IDs to their associated metadata. - metadata map[PackageID]*KnownMetadata - - // importedBy maps package IDs to the list of packages that import them. - importedBy map[PackageID][]PackageID -} - -func NewMetadataGraph() *metadataGraph { - return &metadataGraph{ - ids: make(map[span.URI][]PackageID), - metadata: make(map[PackageID]*KnownMetadata), - importedBy: make(map[PackageID][]PackageID), - } -} diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go deleted file mode 100644 index 96c2a0733a5..00000000000 --- a/internal/lsp/cache/load.go +++ /dev/null @@ -1,563 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - -import ( - "bytes" - "context" - "crypto/sha256" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/memoize" - "golang.org/x/tools/internal/packagesinternal" - "golang.org/x/tools/internal/span" -) - -// load calls packages.Load for the given scopes, updating package metadata, -// import graph, and mapped files with the result. -// -// The resulting error may wrap the moduleErrorMap error type, representing -// errors associated with specific modules. -func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) { - var query []string - var containsDir bool // for logging - - // Keep track of module query -> module path so that we can later correlate query - // errors with errors. - moduleQueries := make(map[string]string) - for _, scope := range scopes { - if !s.shouldLoad(scope) { - continue - } - // Unless the context was canceled, set "shouldLoad" to false for all - // of the metadata we attempted to load. - defer func() { - if errors.Is(err, context.Canceled) { - return - } - s.clearShouldLoad(scope) - }() - switch scope := scope.(type) { - case PackagePath: - if source.IsCommandLineArguments(string(scope)) { - panic("attempted to load command-line-arguments") - } - // The only time we pass package paths is when we're doing a - // partial workspace load. In those cases, the paths came back from - // go list and should already be GOPATH-vendorized when appropriate. - query = append(query, string(scope)) - case fileURI: - uri := span.URI(scope) - // Don't try to load a file that doesn't exist. - fh := s.FindFile(uri) - if fh == nil || s.View().FileKind(fh) != source.Go { - continue - } - query = append(query, fmt.Sprintf("file=%s", uri.Filename())) - case moduleLoadScope: - switch scope { - case "std", "cmd": - query = append(query, string(scope)) - default: - modQuery := fmt.Sprintf("%s/...", scope) - query = append(query, modQuery) - moduleQueries[modQuery] = string(scope) - } - case viewLoadScope: - // If we are outside of GOPATH, a module, or some other known - // build system, don't load subdirectories. - if !s.ValidBuildConfiguration() { - query = append(query, "./") - } else { - query = append(query, "./...") - } - default: - panic(fmt.Sprintf("unknown scope type %T", scope)) - } - switch scope.(type) { - case viewLoadScope, moduleLoadScope: - containsDir = true - } - } - if len(query) == 0 { - return nil - } - sort.Strings(query) // for determinism - - if s.view.Options().VerboseWorkDoneProgress { - work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading query=%s", query), nil, nil) - defer func() { - work.End(ctx, "Done.") - }() - } - - ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query)) - defer done() - - flags := source.LoadWorkspace - if allowNetwork { - flags |= source.AllowNetwork - } - _, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{ - WorkingDir: s.view.rootURI.Filename(), - }) - if err != nil { - return err - } - - // Set a last resort deadline on packages.Load since it calls the go - // command, which may hang indefinitely if it has a bug. golang/go#42132 - // and golang/go#42255 have more context. - ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) - defer cancel() - - cfg := s.config(ctx, inv) - pkgs, err := packages.Load(cfg, query...) - cleanup() - - // If the context was canceled, return early. Otherwise, we might be - // type-checking an incomplete result. Check the context directly, - // because go/packages adds extra information to the error. - if ctx.Err() != nil { - return ctx.Err() - } - if err != nil { - event.Error(ctx, "go/packages.Load", err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) - } else { - event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) - } - if len(pkgs) == 0 { - if err == nil { - err = fmt.Errorf("no packages returned") - } - return fmt.Errorf("%v: %w", err, source.PackagesLoadError) - } - - moduleErrs := make(map[string][]packages.Error) // module path -> errors - for _, pkg := range pkgs { - // The Go command returns synthetic list results for module queries that - // encountered module errors. - // - // For example, given a module path a.mod, we'll query for "a.mod/..." and - // the go command will return a package named "a.mod/..." holding this - // error. Save it for later interpretation. - // - // See golang/go#50862 for more details. - if mod := moduleQueries[pkg.PkgPath]; mod != "" { // a synthetic result for the unloadable module - if len(pkg.Errors) > 0 { - moduleErrs[mod] = pkg.Errors - } - continue - } - - if !containsDir || s.view.Options().VerboseOutput { - event.Log(ctx, "go/packages.Load", - tag.Snapshot.Of(s.ID()), - tag.Package.Of(pkg.ID), - tag.Files.Of(pkg.CompiledGoFiles)) - } - // Ignore packages with no sources, since we will never be able to - // correctly invalidate that metadata. - if len(pkg.GoFiles) == 0 && len(pkg.CompiledGoFiles) == 0 { - continue - } - // Special case for the builtin package, as it has no dependencies. - if pkg.PkgPath == "builtin" { - if len(pkg.GoFiles) != 1 { - return fmt.Errorf("only expected 1 file for builtin, got %v", len(pkg.GoFiles)) - } - s.setBuiltin(pkg.GoFiles[0]) - continue - } - // Skip test main packages. - if isTestMain(pkg, s.view.gocache) { - continue - } - // Skip filtered packages. They may be added anyway if they're - // dependencies of non-filtered packages. - if s.view.allFilesExcluded(pkg) { - continue - } - // Set the metadata for this package. - s.mu.Lock() - m, err := s.setMetadataLocked(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, map[PackageID]struct{}{}) - s.mu.Unlock() - if err != nil { - return err - } - if _, err := s.buildPackageHandle(ctx, m.ID, s.workspaceParseMode(m.ID)); err != nil { - return err - } - } - // Rebuild the import graph when the metadata is updated. - s.clearAndRebuildImportGraph() - - if len(moduleErrs) > 0 { - return &moduleErrorMap{moduleErrs} - } - - return nil -} - -type moduleErrorMap struct { - errs map[string][]packages.Error // module path -> errors -} - -func (m *moduleErrorMap) Error() string { - var paths []string // sort for stability - for path, errs := range m.errs { - if len(errs) > 0 { // should always be true, but be cautious - paths = append(paths, path) - } - } - sort.Strings(paths) - - var buf bytes.Buffer - fmt.Fprintf(&buf, "%d modules have errors:\n", len(paths)) - for _, path := range paths { - fmt.Fprintf(&buf, "\t%s", m.errs[path][0].Msg) - } - - return buf.String() -} - -// workspaceLayoutErrors returns a diagnostic for every open file, as well as -// an error message if there are no open files. -func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError { - if len(s.workspace.getKnownModFiles()) == 0 { - return nil - } - if s.view.userGo111Module == off { - return nil - } - if s.workspace.moduleSource != legacyWorkspace { - return nil - } - // If the user has one module per view, there is nothing to warn about. - if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 { - return nil - } - - // Apply diagnostics about the workspace configuration to relevant open - // files. - openFiles := s.openFiles() - - // If the snapshot does not have a valid build configuration, it may be - // that the user has opened a directory that contains multiple modules. - // Check for that an warn about it. - if !s.ValidBuildConfiguration() { - msg := `gopls requires a module at the root of your workspace. -You can work with multiple modules by opening each one as a workspace folder. -Improvements to this workflow will be coming soon, and you can learn more here: -https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` - return &source.CriticalError{ - MainError: fmt.Errorf(msg), - DiagList: s.applyCriticalErrorToFiles(ctx, msg, openFiles), - } - } - - // If the user has one active go.mod file, they may still be editing files - // in nested modules. Check the module of each open file and add warnings - // that the nested module must be opened as a workspace folder. - if len(s.workspace.getActiveModFiles()) == 1 { - // Get the active root go.mod file to compare against. - var rootModURI span.URI - for uri := range s.workspace.getActiveModFiles() { - rootModURI = uri - } - nestedModules := map[string][]source.VersionedFileHandle{} - for _, fh := range openFiles { - modURI := moduleForURI(s.workspace.knownModFiles, fh.URI()) - if modURI != rootModURI { - modDir := filepath.Dir(modURI.Filename()) - nestedModules[modDir] = append(nestedModules[modDir], fh) - } - } - // Add a diagnostic to each file in a nested module to mark it as - // "orphaned". Don't show a general diagnostic in the progress bar, - // because the user may still want to edit a file in a nested module. - var srcDiags []*source.Diagnostic - for modDir, uris := range nestedModules { - msg := fmt.Sprintf(`This file is in %s, which is a nested module in the %s module. -gopls currently requires one module per workspace folder. -Please open %s as a separate workspace folder. -You can learn more here: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md. -`, modDir, filepath.Dir(rootModURI.Filename()), modDir) - srcDiags = append(srcDiags, s.applyCriticalErrorToFiles(ctx, msg, uris)...) - } - if len(srcDiags) != 0 { - return &source.CriticalError{ - MainError: fmt.Errorf(`You are working in a nested module. -Please open it as a separate workspace folder. Learn more: -https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`), - DiagList: srcDiags, - } - } - } - return nil -} - -func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Diagnostic { - var srcDiags []*source.Diagnostic - for _, fh := range files { - // Place the diagnostics on the package or module declarations. - var rng protocol.Range - switch s.view.FileKind(fh) { - case source.Go: - if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil { - pkgDecl := span.NewRange(s.FileSet(), pgf.File.Package, pgf.File.Name.End()) - if spn, err := pkgDecl.Span(); err == nil { - rng, _ = pgf.Mapper.Range(spn) - } - } - case source.Mod: - if pmf, err := s.ParseMod(ctx, fh); err == nil { - if pmf.File.Module != nil && pmf.File.Module.Syntax != nil { - rng, _ = rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End) - } - } - } - srcDiags = append(srcDiags, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityError, - Source: source.ListError, - Message: msg, - }) - } - return srcDiags -} - -type workspaceDirKey string - -type workspaceDirData struct { - dir string - err error -} - -// getWorkspaceDir gets the URI for the workspace directory associated with -// this snapshot. The workspace directory is a temp directory containing the -// go.mod file computed from all active modules. -func (s *snapshot) getWorkspaceDir(ctx context.Context) (span.URI, error) { - s.mu.Lock() - h := s.workspaceDirHandle - s.mu.Unlock() - if h != nil { - return getWorkspaceDir(ctx, h, s.generation) - } - file, err := s.workspace.modFile(ctx, s) - if err != nil { - return "", err - } - hash := sha256.New() - modContent, err := file.Format() - if err != nil { - return "", err - } - sumContent, err := s.workspace.sumFile(ctx, s) - if err != nil { - return "", err - } - hash.Write(modContent) - hash.Write(sumContent) - key := workspaceDirKey(hash.Sum(nil)) - s.mu.Lock() - h = s.generation.Bind(key, func(context.Context, memoize.Arg) interface{} { - tmpdir, err := ioutil.TempDir("", "gopls-workspace-mod") - if err != nil { - return &workspaceDirData{err: err} - } - - for name, content := range map[string][]byte{ - "go.mod": modContent, - "go.sum": sumContent, - } { - filename := filepath.Join(tmpdir, name) - if err := ioutil.WriteFile(filename, content, 0644); err != nil { - os.RemoveAll(tmpdir) - return &workspaceDirData{err: err} - } - } - - return &workspaceDirData{dir: tmpdir} - }, func(v interface{}) { - d := v.(*workspaceDirData) - if d.dir != "" { - if err := os.RemoveAll(d.dir); err != nil { - event.Error(context.Background(), "cleaning workspace dir", err) - } - } - }) - s.workspaceDirHandle = h - s.mu.Unlock() - return getWorkspaceDir(ctx, h, s.generation) -} - -func getWorkspaceDir(ctx context.Context, h *memoize.Handle, g *memoize.Generation) (span.URI, error) { - v, err := h.Get(ctx, g, nil) - if err != nil { - return "", err - } - return span.URIFromPath(v.(*workspaceDirData).dir), nil -} - -// setMetadataLocked extracts metadata from pkg and records it in s. It -// recurs through pkg.Imports to ensure that metadata exists for all -// dependencies. -func (s *snapshot) setMetadataLocked(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, seen map[PackageID]struct{}) (*Metadata, error) { - id := PackageID(pkg.ID) - if source.IsCommandLineArguments(pkg.ID) { - suffix := ":" + strings.Join(query, ",") - id = PackageID(string(id) + suffix) - pkgPath = PackagePath(string(pkgPath) + suffix) - } - if _, ok := seen[id]; ok { - return nil, fmt.Errorf("import cycle detected: %q", id) - } - // Recreate the metadata rather than reusing it to avoid locking. - m := &Metadata{ - ID: id, - PkgPath: pkgPath, - Name: PackageName(pkg.Name), - ForTest: PackagePath(packagesinternal.GetForTest(pkg)), - TypesSizes: pkg.TypesSizes, - Config: cfg, - Module: pkg.Module, - depsErrors: packagesinternal.GetDepsErrors(pkg), - } - - // Identify intermediate test variants for later filtering. See the - // documentation of IsIntermediateTestVariant for more information. - if m.ForTest != "" && m.ForTest != m.PkgPath && m.ForTest+"_test" != m.PkgPath { - m.IsIntermediateTestVariant = true - } - - for _, err := range pkg.Errors { - // Filter out parse errors from go list. We'll get them when we - // actually parse, and buggy overlay support may generate spurious - // errors. (See TestNewModule_Issue38207.) - if strings.Contains(err.Msg, "expected '") { - continue - } - m.Errors = append(m.Errors, err) - } - - uris := map[span.URI]struct{}{} - for _, filename := range pkg.CompiledGoFiles { - uri := span.URIFromPath(filename) - m.CompiledGoFiles = append(m.CompiledGoFiles, uri) - uris[uri] = struct{}{} - } - for _, filename := range pkg.GoFiles { - uri := span.URIFromPath(filename) - m.GoFiles = append(m.GoFiles, uri) - uris[uri] = struct{}{} - } - s.updateIDForURIsLocked(id, uris) - - // TODO(rstambler): is this still necessary? - copied := map[PackageID]struct{}{ - id: {}, - } - for k, v := range seen { - copied[k] = v - } - for importPath, importPkg := range pkg.Imports { - importPkgPath := PackagePath(importPath) - importID := PackageID(importPkg.ID) - - m.Deps = append(m.Deps, importID) - - // Don't remember any imports with significant errors. - if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 { - if m.MissingDeps == nil { - m.MissingDeps = make(map[PackagePath]struct{}) - } - m.MissingDeps[importPkgPath] = struct{}{} - continue - } - if s.noValidMetadataForIDLocked(importID) { - if _, err := s.setMetadataLocked(ctx, importPkgPath, importPkg, cfg, query, copied); err != nil { - event.Error(ctx, "error in dependency", err) - } - } - } - - // Add the metadata to the cache. - - // If we've already set the metadata for this snapshot, reuse it. - if original, ok := s.meta.metadata[m.ID]; ok && original.Valid { - // Since we've just reloaded, clear out shouldLoad. - original.ShouldLoad = false - m = original.Metadata - } else { - s.meta.metadata[m.ID] = &KnownMetadata{ - Metadata: m, - Valid: true, - } - // Invalidate any packages we may have associated with this metadata. - for _, mode := range []source.ParseMode{source.ParseHeader, source.ParseExported, source.ParseFull} { - key := packageKey{mode, m.ID} - delete(s.packages, key) - } - } - - // Set the workspace packages. If any of the package's files belong to the - // view, then the package may be a workspace package. - for _, uri := range append(m.CompiledGoFiles, m.GoFiles...) { - if !s.view.contains(uri) { - continue - } - - // The package's files are in this view. It may be a workspace package. - if strings.Contains(string(uri), "/vendor/") { - // Vendored packages are not likely to be interesting to the user. - continue - } - - switch { - case m.ForTest == "": - // A normal package. - s.workspacePackages[m.ID] = pkgPath - case m.ForTest == m.PkgPath, m.ForTest+"_test" == m.PkgPath: - // The test variant of some workspace package or its x_test. - // To load it, we need to load the non-test variant with -test. - s.workspacePackages[m.ID] = m.ForTest - } - } - return m, nil -} - -func isTestMain(pkg *packages.Package, gocache string) bool { - // Test mains must have an import path that ends with ".test". - if !strings.HasSuffix(pkg.PkgPath, ".test") { - return false - } - // Test main packages are always named "main". - if pkg.Name != "main" { - return false - } - // Test mains always have exactly one GoFile that is in the build cache. - if len(pkg.GoFiles) > 1 { - return false - } - if !source.InDir(gocache, pkg.GoFiles[0]) { - return false - } - return true -} diff --git a/internal/lsp/cache/metadata.go b/internal/lsp/cache/metadata.go deleted file mode 100644 index c2a21969d88..00000000000 --- a/internal/lsp/cache/metadata.go +++ /dev/null @@ -1,97 +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 cache - -import ( - "go/types" - - "golang.org/x/tools/go/packages" - "golang.org/x/tools/internal/packagesinternal" - "golang.org/x/tools/internal/span" -) - -// Declare explicit types for package paths, names, and IDs to ensure that we -// never use an ID where a path belongs, and vice versa. If we confused these, -// it would result in confusing errors because package IDs often look like -// package paths. -type ( - PackageID string - PackagePath string - PackageName string -) - -// Metadata holds package Metadata extracted from a call to packages.Load. -type Metadata struct { - ID PackageID - PkgPath PackagePath - Name PackageName - GoFiles []span.URI - CompiledGoFiles []span.URI - ForTest PackagePath - TypesSizes types.Sizes - Errors []packages.Error - Deps []PackageID - MissingDeps map[PackagePath]struct{} - Module *packages.Module - depsErrors []*packagesinternal.PackageError - - // Config is the *packages.Config associated with the loaded package. - Config *packages.Config - - // IsIntermediateTestVariant reports whether the given package is an - // intermediate test variant, e.g. - // "golang.org/x/tools/internal/lsp/cache [golang.org/x/tools/internal/lsp/source.test]". - // - // Such test variants arise when an x_test package (in this case source_test) - // imports a package (in this case cache) that itself imports the the - // non-x_test package (in this case source). - // - // This is done so that the forward transitive closure of source_test has - // only one package for the "golang.org/x/tools/internal/lsp/source" import. - // The intermediate test variant exists to hold the test variant import: - // - // golang.org/x/tools/internal/lsp/source_test [golang.org/x/tools/internal/lsp/source.test] - // | "golang.org/x/tools/internal/lsp/cache" -> golang.org/x/tools/internal/lsp/cache [golang.org/x/tools/internal/lsp/source.test] - // | "golang.org/x/tools/internal/lsp/source" -> golang.org/x/tools/internal/lsp/source [golang.org/x/tools/internal/lsp/source.test] - // | ... - // - // golang.org/x/tools/internal/lsp/cache [golang.org/x/tools/internal/lsp/source.test] - // | "golang.org/x/tools/internal/lsp/source" -> golang.org/x/tools/internal/lsp/source [golang.org/x/tools/internal/lsp/source.test] - // | ... - // - // We filter these variants out in certain places. For example, there is - // generally no reason to run diagnostics or analysis on them. - // - // TODO(rfindley): this can probably just be a method, since it is derived - // from other fields. - IsIntermediateTestVariant bool -} - -// Name implements the source.Metadata interface. -func (m *Metadata) PackageName() string { - return string(m.Name) -} - -// PkgPath implements the source.Metadata interface. -func (m *Metadata) PackagePath() string { - return string(m.PkgPath) -} - -// ModuleInfo implements the source.Metadata interface. -func (m *Metadata) ModuleInfo() *packages.Module { - return m.Module -} - -// KnownMetadata is a wrapper around metadata that tracks its validity. -type KnownMetadata struct { - *Metadata - - // Valid is true if the given metadata is Valid. - // Invalid metadata can still be used if a metadata reload fails. - Valid bool - - // ShouldLoad is true if the given metadata should be reloaded. - ShouldLoad bool -} diff --git a/internal/lsp/cache/pkg.go b/internal/lsp/cache/pkg.go deleted file mode 100644 index 1217ec29fd4..00000000000 --- a/internal/lsp/cache/pkg.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - -import ( - "fmt" - "go/ast" - "go/scanner" - "go/types" - - "golang.org/x/mod/module" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" -) - -// pkg contains the type information needed by the source package. -type pkg struct { - m *Metadata - mode source.ParseMode - goFiles []*source.ParsedGoFile - compiledGoFiles []*source.ParsedGoFile - diagnostics []*source.Diagnostic - imports map[PackagePath]*pkg - version *module.Version - parseErrors []scanner.ErrorList - typeErrors []types.Error - types *types.Package - typesInfo *types.Info - typesSizes types.Sizes - hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors -} - -// Declare explicit types for files and directories to distinguish between the two. -type ( - fileURI span.URI - moduleLoadScope string - viewLoadScope span.URI -) - -func (p *pkg) ID() string { - return string(p.m.ID) -} - -func (p *pkg) Name() string { - return string(p.m.Name) -} - -func (p *pkg) PkgPath() string { - return string(p.m.PkgPath) -} - -func (p *pkg) ParseMode() source.ParseMode { - return p.mode -} - -func (p *pkg) CompiledGoFiles() []*source.ParsedGoFile { - return p.compiledGoFiles -} - -func (p *pkg) File(uri span.URI) (*source.ParsedGoFile, error) { - for _, cgf := range p.compiledGoFiles { - if cgf.URI == uri { - return cgf, nil - } - } - for _, gf := range p.goFiles { - if gf.URI == uri { - return gf, nil - } - } - return nil, fmt.Errorf("no parsed file for %s in %v", uri, p.m.ID) -} - -func (p *pkg) GetSyntax() []*ast.File { - var syntax []*ast.File - for _, pgf := range p.compiledGoFiles { - syntax = append(syntax, pgf.File) - } - return syntax -} - -func (p *pkg) GetTypes() *types.Package { - return p.types -} - -func (p *pkg) GetTypesInfo() *types.Info { - return p.typesInfo -} - -func (p *pkg) GetTypesSizes() types.Sizes { - return p.typesSizes -} - -func (p *pkg) IsIllTyped() bool { - return p.types == nil || p.typesInfo == nil || p.typesSizes == nil -} - -func (p *pkg) ForTest() string { - return string(p.m.ForTest) -} - -func (p *pkg) GetImport(pkgPath string) (source.Package, error) { - if imp := p.imports[PackagePath(pkgPath)]; imp != nil { - return imp, nil - } - // Don't return a nil pointer because that still satisfies the interface. - return nil, fmt.Errorf("no imported package for %s", pkgPath) -} - -func (p *pkg) MissingDependencies() []string { - // We don't invalidate metadata for import deletions, so check the package - // imports via the *types.Package. Only use metadata if p.types is nil. - if p.types == nil { - var md []string - for i := range p.m.MissingDeps { - md = append(md, string(i)) - } - return md - } - var md []string - for _, pkg := range p.types.Imports() { - if _, ok := p.m.MissingDeps[PackagePath(pkg.Path())]; ok { - md = append(md, pkg.Path()) - } - } - return md -} - -func (p *pkg) Imports() []source.Package { - var result []source.Package - for _, imp := range p.imports { - result = append(result, imp) - } - return result -} - -func (p *pkg) Version() *module.Version { - return p.version -} - -func (p *pkg) HasListOrParseErrors() bool { - return len(p.m.Errors) != 0 || len(p.parseErrors) != 0 -} - -func (p *pkg) HasTypeErrors() bool { - return len(p.typeErrors) != 0 -} diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go deleted file mode 100644 index c44bd5722cb..00000000000 --- a/internal/lsp/cmd/cmd_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmd_test - -import ( - "os" - "testing" - - "golang.org/x/tools/internal/lsp/bug" - cmdtest "golang.org/x/tools/internal/lsp/cmd/test" - "golang.org/x/tools/internal/lsp/tests" - "golang.org/x/tools/internal/testenv" -) - -func TestMain(m *testing.M) { - bug.PanicOnBugs = true - testenv.ExitIfSmallMachine() - os.Exit(m.Run()) -} - -func TestCommandLine(t *testing.T) { - cmdtest.TestCommandLine(t, "../testdata", tests.DefaultOptions) -} diff --git a/internal/lsp/cmd/export_test.go b/internal/lsp/cmd/export_test.go deleted file mode 100644 index 05b3cd31261..00000000000 --- a/internal/lsp/cmd/export_test.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmd - -const ( - ExampleLine = exampleLine - ExampleColumn = exampleColumn - ExampleOffset = exampleOffset -) diff --git a/internal/lsp/cmd/test/check.go b/internal/lsp/cmd/test/check.go deleted file mode 100644 index 6a53925051f..00000000000 --- a/internal/lsp/cmd/test/check.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cmdtest - -import ( - "fmt" - "io/ioutil" - "strings" - "testing" - - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/span" -) - -func (r *runner) Diagnostics(t *testing.T, uri span.URI, want []*source.Diagnostic) { - if len(want) == 1 && want[0].Message == "" { - return - } - fname := uri.Filename() - out, _ := r.runGoplsCmd(t, "check", fname) - // parse got into a collection of reports - got := map[string]struct{}{} - for _, l := range strings.Split(out, "\n") { - if len(l) == 0 { - continue - } - // parse and reprint to normalize the span - bits := strings.SplitN(l, ": ", 2) - if len(bits) == 2 { - spn := span.Parse(strings.TrimSpace(bits[0])) - spn = span.New(spn.URI(), spn.Start(), span.Point{}) - data, err := ioutil.ReadFile(fname) - if err != nil { - t.Fatal(err) - } - converter := span.NewTokenFile(fname, data) - s, err := spn.WithPosition(converter) - if err != nil { - t.Fatal(err) - } - l = fmt.Sprintf("%s: %s", s, strings.TrimSpace(bits[1])) - } - got[r.NormalizePrefix(l)] = struct{}{} - } - for _, diag := range want { - expect := fmt.Sprintf("%v:%v:%v: %v", uri.Filename(), diag.Range.Start.Line+1, diag.Range.Start.Character+1, diag.Message) - if diag.Range.Start.Character == 0 { - expect = fmt.Sprintf("%v:%v: %v", uri.Filename(), diag.Range.Start.Line+1, diag.Message) - } - expect = r.NormalizePrefix(expect) - _, found := got[expect] - if !found { - t.Errorf("missing diagnostic %q, %v", expect, got) - } else { - delete(got, expect) - } - } - for extra := range got { - t.Errorf("extra diagnostic %q", extra) - } -} diff --git a/internal/lsp/diff/diff.go b/internal/lsp/diff/diff.go deleted file mode 100644 index 8fd6824e530..00000000000 --- a/internal/lsp/diff/diff.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package diff supports a pluggable diff algorithm. -package diff - -import ( - "sort" - "strings" - - "golang.org/x/tools/internal/span" -) - -// TextEdit represents a change to a section of a document. -// The text within the specified span should be replaced by the supplied new text. -type TextEdit struct { - Span span.Span - NewText string -} - -// ComputeEdits is the type for a function that produces a set of edits that -// convert from the before content to the after content. -type ComputeEdits func(uri span.URI, before, after string) ([]TextEdit, error) - -// SortTextEdits attempts to order all edits by their starting points. -// The sort is stable so that edits with the same starting point will not -// be reordered. -func SortTextEdits(d []TextEdit) { - // Use a stable sort to maintain the order of edits inserted at the same position. - sort.SliceStable(d, func(i int, j int) bool { - return span.Compare(d[i].Span, d[j].Span) < 0 - }) -} - -// ApplyEdits applies the set of edits to the before and returns the resulting -// content. -// It may panic or produce garbage if the edits are not valid for the provided -// before content. -func ApplyEdits(before string, edits []TextEdit) string { - // Preconditions: - // - all of the edits apply to before - // - and all the spans for each TextEdit have the same URI - if len(edits) == 0 { - return before - } - edits, _ = prepareEdits(before, edits) - after := strings.Builder{} - last := 0 - for _, edit := range edits { - start := edit.Span.Start().Offset() - if start > last { - after.WriteString(before[last:start]) - last = start - } - after.WriteString(edit.NewText) - last = edit.Span.End().Offset() - } - if last < len(before) { - after.WriteString(before[last:]) - } - return after.String() -} - -// LineEdits takes a set of edits and expands and merges them as necessary -// to ensure that there are only full line edits left when it is done. -func LineEdits(before string, edits []TextEdit) []TextEdit { - if len(edits) == 0 { - return nil - } - edits, partial := prepareEdits(before, edits) - if partial { - edits = lineEdits(before, edits) - } - return edits -} - -// prepareEdits returns a sorted copy of the edits -func prepareEdits(before string, edits []TextEdit) ([]TextEdit, bool) { - partial := false - tf := span.NewTokenFile("", []byte(before)) - copied := make([]TextEdit, len(edits)) - for i, edit := range edits { - edit.Span, _ = edit.Span.WithAll(tf) - copied[i] = edit - partial = partial || - edit.Span.Start().Offset() >= len(before) || - edit.Span.Start().Column() > 1 || edit.Span.End().Column() > 1 - } - SortTextEdits(copied) - return copied, partial -} - -// lineEdits rewrites the edits to always be full line edits -func lineEdits(before string, edits []TextEdit) []TextEdit { - adjusted := make([]TextEdit, 0, len(edits)) - current := TextEdit{Span: span.Invalid} - for _, edit := range edits { - if current.Span.IsValid() && edit.Span.Start().Line() <= current.Span.End().Line() { - // overlaps with the current edit, need to combine - // first get the gap from the previous edit - gap := before[current.Span.End().Offset():edit.Span.Start().Offset()] - // now add the text of this edit - current.NewText += gap + edit.NewText - // and then adjust the end position - current.Span = span.New(current.Span.URI(), current.Span.Start(), edit.Span.End()) - } else { - // does not overlap, add previous run (if there is one) - adjusted = addEdit(before, adjusted, current) - // and then remember this edit as the start of the next run - current = edit - } - } - // add the current pending run if there is one - return addEdit(before, adjusted, current) -} - -func addEdit(before string, edits []TextEdit, edit TextEdit) []TextEdit { - if !edit.Span.IsValid() { - return edits - } - // if edit is partial, expand it to full line now - start := edit.Span.Start() - end := edit.Span.End() - if start.Column() > 1 { - // prepend the text and adjust to start of line - delta := start.Column() - 1 - start = span.NewPoint(start.Line(), 1, start.Offset()-delta) - edit.Span = span.New(edit.Span.URI(), start, end) - edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText - } - if start.Offset() >= len(before) && start.Line() > 1 && before[len(before)-1] != '\n' { - // after end of file that does not end in eol, so join to last line of file - // to do this we need to know where the start of the last line was - eol := strings.LastIndex(before, "\n") - if eol < 0 { - // file is one non terminated line - eol = 0 - } - delta := len(before) - eol - start = span.NewPoint(start.Line()-1, 1, start.Offset()-delta) - edit.Span = span.New(edit.Span.URI(), start, end) - edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText - } - if end.Column() > 1 { - remains := before[end.Offset():] - eol := strings.IndexRune(remains, '\n') - if eol < 0 { - eol = len(remains) - } else { - eol++ - } - end = span.NewPoint(end.Line()+1, 1, end.Offset()+eol) - edit.Span = span.New(edit.Span.URI(), start, end) - edit.NewText = edit.NewText + remains[:eol] - } - edits = append(edits, edit) - return edits -} diff --git a/internal/lsp/diff/diff_test.go b/internal/lsp/diff/diff_test.go deleted file mode 100644 index dd9414e5d7a..00000000000 --- a/internal/lsp/diff/diff_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package diff_test - -import ( - "fmt" - "testing" - - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/diff/difftest" - "golang.org/x/tools/internal/span" -) - -func TestApplyEdits(t *testing.T) { - for _, tc := range difftest.TestCases { - t.Run(tc.Name, func(t *testing.T) { - t.Helper() - if got := diff.ApplyEdits(tc.In, tc.Edits); got != tc.Out { - t.Errorf("ApplyEdits edits got %q, want %q", got, tc.Out) - } - if tc.LineEdits != nil { - if got := diff.ApplyEdits(tc.In, tc.LineEdits); got != tc.Out { - t.Errorf("ApplyEdits lineEdits got %q, want %q", got, tc.Out) - } - } - }) - } -} - -func TestLineEdits(t *testing.T) { - for _, tc := range difftest.TestCases { - t.Run(tc.Name, func(t *testing.T) { - t.Helper() - // if line edits not specified, it is the same as edits - edits := tc.LineEdits - if edits == nil { - edits = tc.Edits - } - if got := diff.LineEdits(tc.In, tc.Edits); diffEdits(got, edits) { - t.Errorf("LineEdits got %q, want %q", got, edits) - } - }) - } -} - -func TestUnified(t *testing.T) { - for _, tc := range difftest.TestCases { - t.Run(tc.Name, func(t *testing.T) { - t.Helper() - unified := fmt.Sprint(diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits)) - if unified != tc.Unified { - t.Errorf("edits got diff:\n%v\nexpected:\n%v", unified, tc.Unified) - } - if tc.LineEdits != nil { - unified := fmt.Sprint(diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.LineEdits)) - if unified != tc.Unified { - t.Errorf("lineEdits got diff:\n%v\nexpected:\n%v", unified, tc.Unified) - } - } - }) - } -} - -func diffEdits(got, want []diff.TextEdit) bool { - if len(got) != len(want) { - return true - } - for i, w := range want { - g := got[i] - if span.Compare(w.Span, g.Span) != 0 { - return true - } - if w.NewText != g.NewText { - return true - } - } - return false -} diff --git a/internal/lsp/diff/difftest/difftest.go b/internal/lsp/diff/difftest/difftest.go deleted file mode 100644 index a78e2674521..00000000000 --- a/internal/lsp/diff/difftest/difftest.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package difftest supplies a set of tests that will operate on any -// implementation of a diff algorithm as exposed by -// "golang.org/x/tools/internal/lsp/diff" -package difftest - -import ( - "fmt" - "testing" - - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/span" -) - -const ( - FileA = "from" - FileB = "to" - UnifiedPrefix = "--- " + FileA + "\n+++ " + FileB + "\n" -) - -var TestCases = []struct { - Name, In, Out, Unified string - Edits, LineEdits []diff.TextEdit - NoDiff bool -}{{ - Name: "empty", - In: "", - Out: "", -}, { - Name: "no_diff", - In: "gargantuan\n", - Out: "gargantuan\n", -}, { - Name: "replace_all", - In: "fruit\n", - Out: "cheese\n", - Unified: UnifiedPrefix + ` -@@ -1 +1 @@ --fruit -+cheese -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(0, 5), NewText: "cheese"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 6), NewText: "cheese\n"}}, -}, { - Name: "insert_rune", - In: "gord\n", - Out: "gourd\n", - Unified: UnifiedPrefix + ` -@@ -1 +1 @@ --gord -+gourd -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(2, 2), NewText: "u"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 5), NewText: "gourd\n"}}, -}, { - Name: "delete_rune", - In: "groat\n", - Out: "goat\n", - Unified: UnifiedPrefix + ` -@@ -1 +1 @@ --groat -+goat -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(1, 2), NewText: ""}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 6), NewText: "goat\n"}}, -}, { - Name: "replace_rune", - In: "loud\n", - Out: "lord\n", - Unified: UnifiedPrefix + ` -@@ -1 +1 @@ --loud -+lord -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(2, 3), NewText: "r"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 5), NewText: "lord\n"}}, -}, { - Name: "replace_partials", - In: "blanket\n", - Out: "bunker\n", - Unified: UnifiedPrefix + ` -@@ -1 +1 @@ --blanket -+bunker -`[1:], - Edits: []diff.TextEdit{ - {Span: newSpan(1, 3), NewText: "u"}, - {Span: newSpan(6, 7), NewText: "r"}, - }, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 8), NewText: "bunker\n"}}, -}, { - Name: "insert_line", - In: "1: one\n3: three\n", - Out: "1: one\n2: two\n3: three\n", - Unified: UnifiedPrefix + ` -@@ -1,2 +1,3 @@ - 1: one -+2: two - 3: three -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(7, 7), NewText: "2: two\n"}}, -}, { - Name: "replace_no_newline", - In: "A", - Out: "B", - Unified: UnifiedPrefix + ` -@@ -1 +1 @@ --A -\ No newline at end of file -+B -\ No newline at end of file -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "B"}}, -}, { - Name: "add_end", - In: "A", - Out: "AB", - Unified: UnifiedPrefix + ` -@@ -1 +1 @@ --A -\ No newline at end of file -+AB -\ No newline at end of file -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(1, 1), NewText: "B"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "AB"}}, -}, { - Name: "add_newline", - In: "A", - Out: "A\n", - Unified: UnifiedPrefix + ` -@@ -1 +1 @@ --A -\ No newline at end of file -+A -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(1, 1), NewText: "\n"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(0, 1), NewText: "A\n"}}, -}, { - Name: "delete_front", - In: "A\nB\nC\nA\nB\nB\nA\n", - Out: "C\nB\nA\nB\nA\nC\n", - Unified: UnifiedPrefix + ` -@@ -1,7 +1,6 @@ --A --B - C -+B - A - B --B - A -+C -`[1:], - Edits: []diff.TextEdit{ - {Span: newSpan(0, 4), NewText: ""}, - {Span: newSpan(6, 6), NewText: "B\n"}, - {Span: newSpan(10, 12), NewText: ""}, - {Span: newSpan(14, 14), NewText: "C\n"}, - }, - NoDiff: true, // diff algorithm produces different delete/insert pattern -}, - { - Name: "replace_last_line", - In: "A\nB\n", - Out: "A\nC\n\n", - Unified: UnifiedPrefix + ` -@@ -1,2 +1,3 @@ - A --B -+C -+ -`[1:], - Edits: []diff.TextEdit{{Span: newSpan(2, 3), NewText: "C\n"}}, - LineEdits: []diff.TextEdit{{Span: newSpan(2, 4), NewText: "C\n\n"}}, - }, - { - Name: "multiple_replace", - In: "A\nB\nC\nD\nE\nF\nG\n", - Out: "A\nH\nI\nJ\nE\nF\nK\n", - Unified: UnifiedPrefix + ` -@@ -1,7 +1,7 @@ - A --B --C --D -+H -+I -+J - E - F --G -+K -`[1:], - Edits: []diff.TextEdit{ - {Span: newSpan(2, 8), NewText: "H\nI\nJ\n"}, - {Span: newSpan(12, 14), NewText: "K\n"}, - }, - NoDiff: true, // diff algorithm produces different delete/insert pattern - }, -} - -func init() { - // expand all the spans to full versions - // we need them all to have their line number and column - for _, tc := range TestCases { - tf := span.NewTokenFile("", []byte(tc.In)) - for i := range tc.Edits { - tc.Edits[i].Span, _ = tc.Edits[i].Span.WithAll(tf) - } - for i := range tc.LineEdits { - tc.LineEdits[i].Span, _ = tc.LineEdits[i].Span.WithAll(tf) - } - } -} - -func DiffTest(t *testing.T, compute diff.ComputeEdits) { - t.Helper() - for _, test := range TestCases { - t.Run(test.Name, func(t *testing.T) { - t.Helper() - edits, err := compute(span.URIFromPath("/"+test.Name), test.In, test.Out) - if err != nil { - t.Fatal(err) - } - got := diff.ApplyEdits(test.In, edits) - unified := fmt.Sprint(diff.ToUnified(FileA, FileB, test.In, edits)) - if got != test.Out { - t.Errorf("got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v", got, unified, test.Out) - } - if !test.NoDiff && unified != test.Unified { - t.Errorf("got diff:\n%v\nexpected:\n%v", unified, test.Unified) - } - }) - } -} - -func newSpan(start, end int) span.Span { - return span.New("", span.NewPoint(0, 0, start), span.NewPoint(0, 0, end)) -} diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go deleted file mode 100644 index 9c49d8b36b1..00000000000 --- a/internal/lsp/mod/diagnostics.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package mod provides core features related to go.mod file -// handling for use by Go editors and tools. -package mod - -import ( - "context" - "fmt" - - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/command" - "golang.org/x/tools/internal/lsp/debug/tag" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" -) - -func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { - ctx, done := event.Start(ctx, "mod.Diagnostics", tag.Snapshot.Of(snapshot.ID())) - defer done() - - reports := map[source.VersionedFileIdentity][]*source.Diagnostic{} - for _, uri := range snapshot.ModFiles() { - fh, err := snapshot.GetVersionedFile(ctx, uri) - if err != nil { - return nil, err - } - reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{} - diagnostics, err := DiagnosticsForMod(ctx, snapshot, fh) - if err != nil { - return nil, err - } - for _, d := range diagnostics { - fh, err := snapshot.GetVersionedFile(ctx, d.URI) - if err != nil { - return nil, err - } - reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], d) - } - } - return reports, nil -} - -func DiagnosticsForMod(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]*source.Diagnostic, error) { - pm, err := snapshot.ParseMod(ctx, fh) - if err != nil { - if pm == nil || len(pm.ParseErrors) == 0 { - return nil, err - } - return pm.ParseErrors, nil - } - - var diagnostics []*source.Diagnostic - - // Add upgrade quick fixes for individual modules if we know about them. - upgrades := snapshot.View().ModuleUpgrades() - for _, req := range pm.File.Require { - ver, ok := upgrades[req.Mod.Path] - if !ok || req.Mod.Version == ver { - continue - } - rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End) - if err != nil { - return nil, err - } - // Upgrade to the exact version we offer the user, not the most recent. - title := fmt.Sprintf("Upgrade to %v", ver) - cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ - URI: protocol.URIFromSpanURI(fh.URI()), - AddRequire: false, - GoCmdArgs: []string{req.Mod.Path + "@" + ver}, - }) - if err != nil { - return nil, err - } - diagnostics = append(diagnostics, &source.Diagnostic{ - URI: fh.URI(), - Range: rng, - Severity: protocol.SeverityInformation, - Source: source.UpgradeNotification, - Message: fmt.Sprintf("%v can be upgraded", req.Mod.Path), - SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, - }) - } - - // Packages in the workspace can contribute diagnostics to go.mod files. - wspkgs, err := snapshot.ActivePackages(ctx) - if err != nil && !source.IsNonFatalGoModError(err) { - event.Error(ctx, fmt.Sprintf("workspace packages: diagnosing %s", pm.URI), err) - } - if err == nil { - for _, pkg := range wspkgs { - pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) - if err != nil { - return nil, err - } - diagnostics = append(diagnostics, pkgDiagnostics[fh.URI()]...) - } - } - - tidied, err := snapshot.ModTidy(ctx, pm) - if err != nil && !source.IsNonFatalGoModError(err) { - event.Error(ctx, fmt.Sprintf("tidy: diagnosing %s", pm.URI), err) - } - if err == nil { - for _, d := range tidied.Diagnostics { - if d.URI != fh.URI() { - continue - } - diagnostics = append(diagnostics, d) - } - } - return diagnostics, nil -} diff --git a/internal/lsp/protocol/span.go b/internal/lsp/protocol/span.go deleted file mode 100644 index 744746d3538..00000000000 --- a/internal/lsp/protocol/span.go +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// this file contains protocol<->span converters - -package protocol - -import ( - "fmt" - "go/token" - - "golang.org/x/tools/internal/span" -) - -// A ColumnMapper maps between UTF-8 oriented positions (e.g. token.Pos, -// span.Span) and the UTF-16 oriented positions used by the LSP. -type ColumnMapper struct { - URI span.URI - TokFile *token.File - Content []byte -} - -// NewColumnMapper creates a new column mapper for the given uri and content. -func NewColumnMapper(uri span.URI, content []byte) *ColumnMapper { - tf := span.NewTokenFile(uri.Filename(), content) - return &ColumnMapper{ - URI: uri, - TokFile: tf, - Content: content, - } -} - -func URIFromSpanURI(uri span.URI) DocumentURI { - return DocumentURI(uri) -} - -func URIFromPath(path string) DocumentURI { - return URIFromSpanURI(span.URIFromPath(path)) -} - -func (u DocumentURI) SpanURI() span.URI { - return span.URIFromURI(string(u)) -} - -func (m *ColumnMapper) Location(s span.Span) (Location, error) { - rng, err := m.Range(s) - if err != nil { - return Location{}, err - } - return Location{URI: URIFromSpanURI(s.URI()), Range: rng}, nil -} - -func (m *ColumnMapper) Range(s span.Span) (Range, error) { - if span.CompareURI(m.URI, s.URI()) != 0 { - return Range{}, fmt.Errorf("column mapper is for file %q instead of %q", m.URI, s.URI()) - } - s, err := s.WithAll(m.TokFile) - if err != nil { - return Range{}, err - } - start, err := m.Position(s.Start()) - if err != nil { - return Range{}, err - } - end, err := m.Position(s.End()) - if err != nil { - return Range{}, err - } - return Range{Start: start, End: end}, nil -} - -func (m *ColumnMapper) Position(p span.Point) (Position, error) { - chr, err := span.ToUTF16Column(p, m.Content) - if err != nil { - return Position{}, err - } - return Position{ - Line: uint32(p.Line() - 1), - Character: uint32(chr - 1), - }, nil -} - -func (m *ColumnMapper) Span(l Location) (span.Span, error) { - return m.RangeSpan(l.Range) -} - -func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) { - start, err := m.Point(r.Start) - if err != nil { - return span.Span{}, err - } - end, err := m.Point(r.End) - if err != nil { - return span.Span{}, err - } - return span.New(m.URI, start, end).WithAll(m.TokFile) -} - -func (m *ColumnMapper) RangeToSpanRange(r Range) (span.Range, error) { - spn, err := m.RangeSpan(r) - if err != nil { - return span.Range{}, err - } - return spn.Range(m.TokFile) -} - -// Pos returns the token.Pos of p within the mapped file. -func (m *ColumnMapper) Pos(p Position) (token.Pos, error) { - start, err := m.Point(p) - if err != nil { - return token.NoPos, err - } - // TODO: refactor the span package to avoid creating this unnecessary end position. - spn, err := span.New(m.URI, start, start).WithAll(m.TokFile) - if err != nil { - return token.NoPos, err - } - rng, err := spn.Range(m.TokFile) - if err != nil { - return token.NoPos, err - } - return rng.Start, nil -} - -// Offset returns the utf-8 byte offset of p within the mapped file. -func (m *ColumnMapper) Offset(p Position) (int, error) { - start, err := m.Point(p) - if err != nil { - return 0, err - } - return start.Offset(), nil -} - -// Point returns a span.Point for p within the mapped file. The resulting point -// always has an Offset. -func (m *ColumnMapper) Point(p Position) (span.Point, error) { - line := int(p.Line) + 1 - offset, err := span.ToOffset(m.TokFile, line, 1) - if err != nil { - return span.Point{}, err - } - lineStart := span.NewPoint(line, 1, offset) - return span.FromUTF16Column(lineStart, int(p.Character)+1, m.Content) -} - -func IsPoint(r Range) bool { - return r.Start.Line == r.End.Line && r.Start.Character == r.End.Character -} - -func CompareRange(a, b Range) int { - if r := ComparePosition(a.Start, b.Start); r != 0 { - return r - } - return ComparePosition(a.End, b.End) -} - -func ComparePosition(a, b Position) int { - if a.Line < b.Line { - return -1 - } - if a.Line > b.Line { - return 1 - } - if a.Character < b.Character { - return -1 - } - if a.Character > b.Character { - return 1 - } - return 0 -} - -func Intersect(a, b Range) bool { - if a.Start.Line > b.End.Line || a.End.Line < b.Start.Line { - return false - } - return !((a.Start.Line == b.End.Line) && a.Start.Character > b.End.Character || - (a.End.Line == b.Start.Line) && a.End.Character < b.Start.Character) -} - -func (r Range) Format(f fmt.State, _ rune) { - fmt.Fprintf(f, "%v:%v-%v:%v", r.Start.Line, r.Start.Character, r.End.Line, r.End.Character) -} diff --git a/internal/lsp/protocol/tsprotocol.go b/internal/lsp/protocol/tsprotocol.go deleted file mode 100644 index 647aabc2ee1..00000000000 --- a/internal/lsp/protocol/tsprotocol.go +++ /dev/null @@ -1,6750 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Code generated (see typescript/README.md) DO NOT EDIT. - -// Package protocol contains data types and code for LSP json rpcs -// generated automatically from vscode-languageserver-node -// commit: 696f9285bf849b73745682fdb1c1feac73eb8772 -// last fetched Fri Apr 01 2022 10:53:41 GMT-0400 (Eastern Daylight Time) -package protocol - -import "encoding/json" - -/** - * A special text edit with an additional change annotation. - * - * @since 3.16.0. - */ -type AnnotatedTextEdit struct { - /** - * The actual identifier of the change annotation - */ - AnnotationID ChangeAnnotationIdentifier `json:"annotationId"` - TextEdit -} - -/** - * The parameters passed via a apply workspace edit request. - */ -type ApplyWorkspaceEditParams struct { - /** - * An optional label of the workspace edit. This label is - * presented in the user interface for example on an undo - * stack to undo the workspace edit. - */ - Label string `json:"label,omitempty"` - /** - * The edits to apply. - */ - Edit WorkspaceEdit `json:"edit"` -} - -/** - * The result returned from the apply workspace edit request. - * - * @since 3.17 renamed from ApplyWorkspaceEditResponse - */ -type ApplyWorkspaceEditResult struct { - /** - * Indicates whether the edit was applied or not. - */ - Applied bool `json:"applied"` - /** - * An optional textual description for why the edit was not applied. - * This may be used by the server for diagnostic logging or to provide - * a suitable error for a request that triggered the edit. - */ - FailureReason string `json:"failureReason,omitempty"` - /** - * Depending on the client's failure handling strategy `failedChange` might - * contain the index of the change that failed. This property is only available - * if the client signals a `failureHandlingStrategy` in its client capabilities. - */ - FailedChange uint32 `json:"failedChange,omitempty"` -} - -/** - * @since 3.16.0 - */ -type CallHierarchyClientCapabilities struct { - /** - * Whether implementation supports dynamic registration. If this is set to `true` - * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` - * return value for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * Represents an incoming call, e.g. a caller of a method or constructor. - * - * @since 3.16.0 - */ -type CallHierarchyIncomingCall struct { - /** - * The item that makes the call. - */ - From CallHierarchyItem `json:"from"` - /** - * The ranges at which the calls appear. This is relative to the caller - * denoted by [`this.from`](#CallHierarchyIncomingCall.from). - */ - FromRanges []Range `json:"fromRanges"` -} - -/** - * The parameter of a `callHierarchy/incomingCalls` request. - * - * @since 3.16.0 - */ -type CallHierarchyIncomingCallsParams struct { - Item CallHierarchyItem `json:"item"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * Represents programming constructs like functions or constructors in the context - * of call hierarchy. - * - * @since 3.16.0 - */ -type CallHierarchyItem struct { - /** - * The name of this item. - */ - Name string `json:"name"` - /** - * The kind of this item. - */ - Kind SymbolKind `json:"kind"` - /** - * Tags for this item. - */ - Tags []SymbolTag `json:"tags,omitempty"` - /** - * More detail for this item, e.g. the signature of a function. - */ - Detail string `json:"detail,omitempty"` - /** - * The resource identifier of this item. - */ - URI DocumentURI `json:"uri"` - /** - * The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. - */ - Range Range `json:"range"` - /** - * The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. - * Must be contained by the [`range`](#CallHierarchyItem.range). - */ - SelectionRange Range `json:"selectionRange"` - /** - * A data entry field that is preserved between a call hierarchy prepare and - * incoming calls or outgoing calls requests. - */ - Data LSPAny `json:"data,omitempty"` -} - -/** - * Call hierarchy options used during static registration. - * - * @since 3.16.0 - */ -type CallHierarchyOptions struct { - WorkDoneProgressOptions -} - -/** - * Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. - * - * @since 3.16.0 - */ -type CallHierarchyOutgoingCall struct { - /** - * The item that is called. - */ - To CallHierarchyItem `json:"to"` - /** - * The range at which this item is called. This is the range relative to the caller, e.g the item - * passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls) - * and not [`this.to`](#CallHierarchyOutgoingCall.to). - */ - FromRanges []Range `json:"fromRanges"` -} - -/** - * The parameter of a `callHierarchy/outgoingCalls` request. - * - * @since 3.16.0 - */ -type CallHierarchyOutgoingCallsParams struct { - Item CallHierarchyItem `json:"item"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * The parameter of a `textDocument/prepareCallHierarchy` request. - * - * @since 3.16.0 - */ -type CallHierarchyPrepareParams struct { - TextDocumentPositionParams - WorkDoneProgressParams -} - -/** - * Call hierarchy options used during static or dynamic registration. - * - * @since 3.16.0 - */ -type CallHierarchyRegistrationOptions struct { - TextDocumentRegistrationOptions - CallHierarchyOptions - StaticRegistrationOptions -} - -type CancelParams struct { - /** - * The request id to cancel. - */ - ID interface{} /*number | string*/ `json:"id"` -} - -/** - * Additional information that describes document changes. - * - * @since 3.16.0 - */ -type ChangeAnnotation struct { - /** - * A human-readable string describing the actual change. The string - * is rendered prominent in the user interface. - */ - Label string `json:"label"` - /** - * A flag which indicates that user confirmation is needed - * before applying the change. - */ - NeedsConfirmation bool `json:"needsConfirmation,omitempty"` - /** - * A human-readable string which is rendered less prominent in - * the user interface. - */ - Description string `json:"description,omitempty"` -} - -/** - * An identifier to refer to a change annotation stored with a workspace edit. - */ -type ChangeAnnotationIdentifier = string - -type ClientCapabilities struct { - /** - * The workspace client capabilities - */ - Workspace Workspace3Gn `json:"workspace,omitempty"` - /** - * Text document specific client capabilities. - */ - TextDocument TextDocumentClientCapabilities `json:"textDocument,omitempty"` - /** - * Window specific client capabilities. - */ - Window struct { - /** - * Whether client supports server initiated progress using the - * `window/workDoneProgress/create` request. - * - * Since 3.15.0 - */ - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` - /** - * Capabilities specific to the showMessage request. - * - * @since 3.16.0 - */ - ShowMessage ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` - /** - * Capabilities specific to the showDocument request. - * - * @since 3.16.0 - */ - ShowDocument ShowDocumentClientCapabilities `json:"showDocument,omitempty"` - } `json:"window,omitempty"` - /** - * General client capabilities. - * - * @since 3.16.0 - */ - General GeneralClientCapabilities `json:"general,omitempty"` - /** - * Experimental client capabilities. - */ - Experimental interface{} `json:"experimental,omitempty"` -} - -/** - * A code action represents a change that can be performed in code, e.g. to fix a problem or - * to refactor code. - * - * A CodeAction must set either `edit` and/or a `command`. If both are supplied, the `edit` is applied first, then the `command` is executed. - */ -type CodeAction struct { - /** - * A short, human-readable, title for this code action. - */ - Title string `json:"title"` - /** - * The kind of the code action. - * - * Used to filter code actions. - */ - Kind CodeActionKind `json:"kind,omitempty"` - /** - * The diagnostics that this code action resolves. - */ - Diagnostics []Diagnostic `json:"diagnostics,omitempty"` - /** - * Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted - * by keybindings. - * - * A quick fix should be marked preferred if it properly addresses the underlying error. - * A refactoring should be marked preferred if it is the most reasonable choice of actions to take. - * - * @since 3.15.0 - */ - IsPreferred bool `json:"isPreferred,omitempty"` - /** - * Marks that the code action cannot currently be applied. - * - * Clients should follow the following guidelines regarding disabled code actions: - * - * - Disabled code actions are not shown in automatic [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) - * code action menu. - * - * - Disabled actions are shown as faded out in the code action menu when the user request a more specific type - * of code action, such as refactorings. - * - * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) - * that auto applies a code action and only a disabled code actions are returned, the client should show the user an - * error message with `reason` in the editor. - * - * @since 3.16.0 - */ - Disabled *struct { - /** - * Human readable description of why the code action is currently disabled. - * - * This is displayed in the code actions UI. - */ - Reason string `json:"reason"` - } `json:"disabled,omitempty"` - /** - * The workspace edit this code action performs. - */ - Edit WorkspaceEdit `json:"edit,omitempty"` - /** - * A command this code action executes. If a code action - * provides a edit and a command, first the edit is - * executed and then the command. - */ - Command *Command `json:"command,omitempty"` - /** - * A data entry field that is preserved on a code action between - * a `textDocument/codeAction` and a `codeAction/resolve` request. - * - * @since 3.16.0 - */ - Data LSPAny `json:"data,omitempty"` -} - -/** - * The Client Capabilities of a [CodeActionRequest](#CodeActionRequest). - */ -type CodeActionClientCapabilities struct { - /** - * Whether code action supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client support code action literals of type `CodeAction` as a valid - * response of the `textDocument/codeAction` request. If the property is not - * set the request can only return `Command` literals. - * - * @since 3.8.0 - */ - CodeActionLiteralSupport struct { - /** - * The code action kind is support with the following value - * set. - */ - CodeActionKind struct { - /** - * The code action kind values the client supports. When this - * property exists the client also guarantees that it will - * handle values outside its set gracefully and falls back - * to a default value when unknown. - */ - ValueSet []CodeActionKind `json:"valueSet"` - } `json:"codeActionKind"` - } `json:"codeActionLiteralSupport,omitempty"` - /** - * Whether code action supports the `isPreferred` property. - * - * @since 3.15.0 - */ - IsPreferredSupport bool `json:"isPreferredSupport,omitempty"` - /** - * Whether code action supports the `disabled` property. - * - * @since 3.16.0 - */ - DisabledSupport bool `json:"disabledSupport,omitempty"` - /** - * Whether code action supports the `data` property which is - * preserved between a `textDocument/codeAction` and a - * `codeAction/resolve` request. - * - * @since 3.16.0 - */ - DataSupport bool `json:"dataSupport,omitempty"` - /** - * Whether the client support resolving additional code action - * properties via a separate `codeAction/resolve` request. - * - * @since 3.16.0 - */ - ResolveSupport struct { - /** - * The properties that a client can resolve lazily. - */ - Properties []string `json:"properties"` - } `json:"resolveSupport,omitempty"` - /** - * Whether th client honors the change annotations in - * text edits and resource operations returned via the - * `CodeAction#edit` property by for example presenting - * the workspace edit in the user interface and asking - * for confirmation. - * - * @since 3.16.0 - */ - HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` -} - -/** - * Contains additional diagnostic information about the context in which - * a [code action](#CodeActionProvider.provideCodeActions) is run. - */ -type CodeActionContext struct { - /** - * An array of diagnostics known on the client side overlapping the range provided to the - * `textDocument/codeAction` request. They are provided so that the server knows which - * errors are currently presented to the user for the given range. There is no guarantee - * that these accurately reflect the error state of the resource. The primary parameter - * to compute code actions is the provided range. - */ - Diagnostics []Diagnostic `json:"diagnostics"` - /** - * Requested kind of actions to return. - * - * Actions not of this kind are filtered out by the client before being shown. So servers - * can omit computing them. - */ - Only []CodeActionKind `json:"only,omitempty"` - /** - * The reason why code actions were requested. - * - * @since 3.17.0 - */ - TriggerKind CodeActionTriggerKind `json:"triggerKind,omitempty"` -} - -/** - * A set of predefined code action kinds - */ -type CodeActionKind string - -/** - * Provider options for a [CodeActionRequest](#CodeActionRequest). - */ -type CodeActionOptions struct { - /** - * CodeActionKinds that this server may return. - * - * The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server - * may list out every specific kind they provide. - */ - CodeActionKinds []CodeActionKind `json:"codeActionKinds,omitempty"` - /** - * The server provides support to resolve additional - * information for a code action. - * - * @since 3.16.0 - */ - ResolveProvider bool `json:"resolveProvider,omitempty"` - WorkDoneProgressOptions -} - -/** - * The parameters of a [CodeActionRequest](#CodeActionRequest). - */ -type CodeActionParams struct { - /** - * The document in which the command was invoked. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The range for which the command was invoked. - */ - Range Range `json:"range"` - /** - * Context carrying additional information. - */ - Context CodeActionContext `json:"context"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * The reason why code actions were requested. - * - * @since 3.17.0 - proposed state - */ -type CodeActionTriggerKind float64 - -/** - * Structure to capture a description for an error code. - * - * @since 3.16.0 - */ -type CodeDescription struct { - /** - * An URI to open with more information about the diagnostic error. - */ - Href URI `json:"href"` -} - -/** - * A code lens represents a [command](#Command) that should be shown along with - * source text, like the number of references, a way to run tests, etc. - * - * A code lens is _unresolved_ when no command is associated to it. For performance - * reasons the creation of a code lens and resolving should be done to two stages. - */ -type CodeLens struct { - /** - * The range in which this code lens is valid. Should only span a single line. - */ - Range Range `json:"range"` - /** - * The command this code lens represents. - */ - Command Command `json:"command,omitempty"` - /** - * A data entry field that is preserved on a code lens item between - * a [CodeLensRequest](#CodeLensRequest) and a [CodeLensResolveRequest] - * (#CodeLensResolveRequest) - */ - Data LSPAny `json:"data,omitempty"` -} - -/** - * The client capabilities of a [CodeLensRequest](#CodeLensRequest). - */ -type CodeLensClientCapabilities struct { - /** - * Whether code lens supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * Code Lens provider options of a [CodeLensRequest](#CodeLensRequest). - */ -type CodeLensOptions struct { - /** - * Code lens has a resolve provider as well. - */ - ResolveProvider bool `json:"resolveProvider,omitempty"` - WorkDoneProgressOptions -} - -/** - * The parameters of a [CodeLensRequest](#CodeLensRequest). - */ -type CodeLensParams struct { - /** - * The document to request code lens for. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * @since 3.16.0 - */ -type CodeLensWorkspaceClientCapabilities struct { - /** - * Whether the client implementation supports a refresh request sent from the - * server to the client. - * - * Note that this event is global and will force the client to refresh all - * code lenses currently shown. It should be used with absolute care and is - * useful for situation where a server for example detect a project wide - * change that requires such a calculation. - */ - RefreshSupport bool `json:"refreshSupport,omitempty"` -} - -/** - * Represents a color in RGBA space. - */ -type Color struct { - /** - * The red component of this color in the range [0-1]. - */ - Red Decimal `json:"red"` - /** - * The green component of this color in the range [0-1]. - */ - Green Decimal `json:"green"` - /** - * The blue component of this color in the range [0-1]. - */ - Blue Decimal `json:"blue"` - /** - * The alpha component of this color in the range [0-1]. - */ - Alpha Decimal `json:"alpha"` -} - -/** - * Represents a color range from a document. - */ -type ColorInformation struct { - /** - * The range in the document where this color appears. - */ - Range Range `json:"range"` - /** - * The actual color value for this color range. - */ - Color Color `json:"color"` -} - -type ColorPresentation struct { - /** - * The label of this color presentation. It will be shown on the color - * picker header. By default this is also the text that is inserted when selecting - * this color presentation. - */ - Label string `json:"label"` - /** - * An [edit](#TextEdit) which is applied to a document when selecting - * this presentation for the color. When `falsy` the [label](#ColorPresentation.label) - * is used. - */ - TextEdit TextEdit `json:"textEdit,omitempty"` - /** - * An optional array of additional [text edits](#TextEdit) that are applied when - * selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves. - */ - AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` -} - -/** - * Parameters for a [ColorPresentationRequest](#ColorPresentationRequest). - */ -type ColorPresentationParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The color to request presentations for. - */ - Color Color `json:"color"` - /** - * The range where the color would be inserted. Serves as a context. - */ - Range Range `json:"range"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * Represents a reference to a command. Provides a title which - * will be used to represent a command in the UI and, optionally, - * an array of arguments which will be passed to the command handler - * function when invoked. - */ -type Command struct { - /** - * Title of the command, like `save`. - */ - Title string `json:"title"` - /** - * The identifier of the actual command handler. - */ - Command string `json:"command"` - /** - * Arguments that the command handler should be - * invoked with. - */ - Arguments []json.RawMessage `json:"arguments,omitempty"` -} - -/** - * Completion client capabilities - */ -type CompletionClientCapabilities struct { - /** - * Whether completion supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports the following `CompletionItem` specific - * capabilities. - */ - CompletionItem struct { - /** - * Client supports snippets as insert text. - * - * A snippet can define tab stops and placeholders with `$1`, `$2` - * and `${3:foo}`. `$0` defines the final tab stop, it defaults to - * the end of the snippet. Placeholders with equal identifiers are linked, - * that is typing in one will update others too. - */ - SnippetSupport bool `json:"snippetSupport,omitempty"` - /** - * Client supports commit characters on a completion item. - */ - CommitCharactersSupport bool `json:"commitCharactersSupport,omitempty"` - /** - * Client supports the follow content formats for the documentation - * property. The order describes the preferred format of the client. - */ - DocumentationFormat []MarkupKind `json:"documentationFormat,omitempty"` - /** - * Client supports the deprecated property on a completion item. - */ - DeprecatedSupport bool `json:"deprecatedSupport,omitempty"` - /** - * Client supports the preselect property on a completion item. - */ - PreselectSupport bool `json:"preselectSupport,omitempty"` - /** - * Client supports the tag property on a completion item. Clients supporting - * tags have to handle unknown tags gracefully. Clients especially need to - * preserve unknown tags when sending a completion item back to the server in - * a resolve call. - * - * @since 3.15.0 - */ - TagSupport struct { - /** - * The tags supported by the client. - */ - ValueSet []CompletionItemTag `json:"valueSet"` - } `json:"tagSupport,omitempty"` - /** - * Client support insert replace edit to control different behavior if a - * completion item is inserted in the text or should replace text. - * - * @since 3.16.0 - */ - InsertReplaceSupport bool `json:"insertReplaceSupport,omitempty"` - /** - * Indicates which properties a client can resolve lazily on a completion - * item. Before version 3.16.0 only the predefined properties `documentation` - * and `details` could be resolved lazily. - * - * @since 3.16.0 - */ - ResolveSupport struct { - /** - * The properties that a client can resolve lazily. - */ - Properties []string `json:"properties"` - } `json:"resolveSupport,omitempty"` - /** - * The client supports the `insertTextMode` property on - * a completion item to override the whitespace handling mode - * as defined by the client (see `insertTextMode`). - * - * @since 3.16.0 - */ - InsertTextModeSupport struct { - ValueSet []InsertTextMode `json:"valueSet"` - } `json:"insertTextModeSupport,omitempty"` - /** - * The client has support for completion item label - * details (see also `CompletionItemLabelDetails`). - * - * @since 3.17.0 - proposed state - */ - LabelDetailsSupport bool `json:"labelDetailsSupport,omitempty"` - } `json:"completionItem,omitempty"` - CompletionItemKind struct { - /** - * The completion item kind values the client supports. When this - * property exists the client also guarantees that it will - * handle values outside its set gracefully and falls back - * to a default value when unknown. - * - * If this property is not present the client only supports - * the completion items kinds from `Text` to `Reference` as defined in - * the initial version of the protocol. - */ - ValueSet []CompletionItemKind `json:"valueSet,omitempty"` - } `json:"completionItemKind,omitempty"` - /** - * Defines how the client handles whitespace and indentation - * when accepting a completion item that uses multi line - * text in either `insertText` or `textEdit`. - * - * @since 3.17.0 - proposed state - */ - InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` - /** - * The client supports to send additional context information for a - * `textDocument/completion` request. - */ - ContextSupport bool `json:"contextSupport,omitempty"` - /** - * The client supports the following `CompletionList` specific - * capabilities. - * - * @since 3.17.0 - proposed state - */ - CompletionList struct { - /** - * The client supports the the following itemDefaults on - * a completion list. - * - * The value lists the supported property names of the - * `CompletionList.itemDefaults` object. If omitted - * no properties are supported. - * - * @since 3.17.0 - proposed state - */ - ItemDefaults []string `json:"itemDefaults,omitempty"` - } `json:"completionList,omitempty"` -} - -/** - * Contains additional information about the context in which a completion request is triggered. - */ -type CompletionContext struct { - /** - * How the completion was triggered. - */ - TriggerKind CompletionTriggerKind `json:"triggerKind"` - /** - * The trigger character (a single character) that has trigger code complete. - * Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` - */ - TriggerCharacter string `json:"triggerCharacter,omitempty"` -} - -/** - * A completion item represents a text snippet that is - * proposed to complete text that is being typed. - */ -type CompletionItem struct { - /** - * The label of this completion item. - * - * The label property is also by default the text that - * is inserted when selecting this completion. - * - * If label details are provided the label itself should - * be an unqualified name of the completion item. - */ - Label string `json:"label"` - /** - * Additional details for the label - * - * @since 3.17.0 - proposed state - */ - LabelDetails CompletionItemLabelDetails `json:"labelDetails,omitempty"` - /** - * The kind of this completion item. Based of the kind - * an icon is chosen by the editor. - */ - Kind CompletionItemKind `json:"kind,omitempty"` - /** - * Tags for this completion item. - * - * @since 3.15.0 - */ - Tags []CompletionItemTag `json:"tags,omitempty"` - /** - * A human-readable string with additional information - * about this item, like type or symbol information. - */ - Detail string `json:"detail,omitempty"` - /** - * A human-readable string that represents a doc-comment. - */ - Documentation string/*string | MarkupContent*/ `json:"documentation,omitempty"` - /** - * Indicates if this item is deprecated. - * @deprecated Use `tags` instead. - */ - Deprecated bool `json:"deprecated,omitempty"` - /** - * Select this item when showing. - * - * *Note* that only one completion item can be selected and that the - * tool / client decides which item that is. The rule is that the *first* - * item of those that match best is selected. - */ - Preselect bool `json:"preselect,omitempty"` - /** - * A string that should be used when comparing this item - * with other items. When `falsy` the [label](#CompletionItem.label) - * is used. - */ - SortText string `json:"sortText,omitempty"` - /** - * A string that should be used when filtering a set of - * completion items. When `falsy` the [label](#CompletionItem.label) - * is used. - */ - FilterText string `json:"filterText,omitempty"` - /** - * A string that should be inserted into a document when selecting - * this completion. When `falsy` the [label](#CompletionItem.label) - * is used. - * - * The `insertText` is subject to interpretation by the client side. - * Some tools might not take the string literally. For example - * VS Code when code complete is requested in this example `con` - * and a completion item with an `insertText` of `console` is provided it - * will only insert `sole`. Therefore it is recommended to use `textEdit` instead - * since it avoids additional client side interpretation. - */ - InsertText string `json:"insertText,omitempty"` - /** - * The format of the insert text. The format applies to both the `insertText` property - * and the `newText` property of a provided `textEdit`. If omitted defaults to - * `InsertTextFormat.PlainText`. - * - * Please note that the insertTextFormat doesn't apply to `additionalTextEdits`. - */ - InsertTextFormat InsertTextFormat `json:"insertTextFormat,omitempty"` - /** - * How whitespace and indentation is handled during completion - * item insertion. If ignored the clients default value depends on - * the `textDocument.completion.insertTextMode` client capability. - * - * @since 3.16.0 - */ - InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` - /** - * An [edit](#TextEdit) which is applied to a document when selecting - * this completion. When an edit is provided the value of - * [insertText](#CompletionItem.insertText) is ignored. - * - * Most editors support two different operation when accepting a completion item. One is to insert a - * completion text and the other is to replace an existing text with a completion text. Since this can - * usually not predetermined by a server it can report both ranges. Clients need to signal support for - * `InsertReplaceEdits` via the `textDocument.completion.insertReplaceSupport` client capability - * property. - * - * *Note 1:* The text edit's range as well as both ranges from a insert replace edit must be a - * [single line] and they must contain the position at which completion has been requested. - * *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range must be a prefix of - * the edit's replace range, that means it must be contained and starting at the same position. - * - * @since 3.16.0 additional type `InsertReplaceEdit` - */ - TextEdit *TextEdit/*TextEdit | InsertReplaceEdit*/ `json:"textEdit,omitempty"` - /** - * An optional array of additional [text edits](#TextEdit) that are applied when - * selecting this completion. Edits must not overlap (including the same insert position) - * with the main [edit](#CompletionItem.textEdit) nor with themselves. - * - * Additional text edits should be used to change text unrelated to the current cursor position - * (for example adding an import statement at the top of the file if the completion item will - * insert an unqualified type). - */ - AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` - /** - * An optional set of characters that when pressed while this completion is active will accept it first and - * then type that character. *Note* that all commit characters should have `length=1` and that superfluous - * characters will be ignored. - */ - CommitCharacters []string `json:"commitCharacters,omitempty"` - /** - * An optional [command](#Command) that is executed *after* inserting this completion. *Note* that - * additional modifications to the current document should be described with the - * [additionalTextEdits](#CompletionItem.additionalTextEdits)-property. - */ - Command *Command `json:"command,omitempty"` - /** - * A data entry field that is preserved on a completion item between a - * [CompletionRequest](#CompletionRequest) and a [CompletionResolveRequest](#CompletionResolveRequest). - */ - Data LSPAny `json:"data,omitempty"` -} - -/** - * The kind of a completion entry. - */ -type CompletionItemKind float64 - -/** - * Additional details for a completion item label. - * - * @since 3.17.0 - proposed state - */ -type CompletionItemLabelDetails struct { - /** - * An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, - * without any spacing. Should be used for function signatures or type annotations. - */ - Detail string `json:"detail,omitempty"` - /** - * An optional string which is rendered less prominently after {@link CompletionItem.detail}. Should be used - * for fully qualified names or file path. - */ - Description string `json:"description,omitempty"` -} - -/** - * Completion item tags are extra annotations that tweak the rendering of a completion - * item. - * - * @since 3.15.0 - */ -type CompletionItemTag float64 - -/** - * Represents a collection of [completion items](#CompletionItem) to be presented - * in the editor. - */ -type CompletionList struct { - /** - * This list it not complete. Further typing results in recomputing this list. - */ - IsIncomplete bool `json:"isIncomplete"` - /** - * In many cases the items of an actual completion result share the same - * value for properties like `commitCharacters` or the range of a text - * edit. A completion list can therefore define item defaults which will - * be used if a completion item itself doesn't specify the value. - * - * If a completion list specifies a default value and a completion item - * also specifies a corresponding value the one from the item is used. - * - * Servers are only allowed to return default values if the client - * signals support for this via the `completionList.itemDefaults` - * capability. - * - * @since 3.17.0 - proposed state - */ - ItemDefaults struct { - /** - * A default commit character set. - * - * @since 3.17.0 - proposed state - */ - CommitCharacters []string `json:"commitCharacters,omitempty"` - /** - * A default edit range - * - * @since 3.17.0 - proposed state - */ - EditRange Range/*Range | { insert: Range; replace: Range; }*/ `json:"editRange,omitempty"` - /** - * A default insert text format - * - * @since 3.17.0 - proposed state - */ - InsertTextFormat InsertTextFormat `json:"insertTextFormat,omitempty"` - /** - * A default insert text mode - * - * @since 3.17.0 - proposed state - */ - InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` - } `json:"itemDefaults,omitempty"` - /** - * The completion items. - */ - Items []CompletionItem `json:"items"` -} - -/** - * Completion options. - */ -type CompletionOptions struct { - /** - * Most tools trigger completion request automatically without explicitly requesting - * it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user - * starts to type an identifier. For example if the user types `c` in a JavaScript file - * code complete will automatically pop up present `console` besides others as a - * completion item. Characters that make up identifiers don't need to be listed here. - * - * If code complete should automatically be trigger on characters not being valid inside - * an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. - */ - TriggerCharacters []string `json:"triggerCharacters,omitempty"` - /** - * The list of all possible characters that commit a completion. This field can be used - * if clients don't support individual commit characters per completion item. See - * `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` - * - * If a server provides both `allCommitCharacters` and commit characters on an individual - * completion item the ones on the completion item win. - * - * @since 3.2.0 - */ - AllCommitCharacters []string `json:"allCommitCharacters,omitempty"` - /** - * The server provides support to resolve additional - * information for a completion item. - */ - ResolveProvider bool `json:"resolveProvider,omitempty"` - /** - * The server supports the following `CompletionItem` specific - * capabilities. - * - * @since 3.17.0 - proposed state - */ - CompletionItem struct { - /** - * The server has support for completion item label - * details (see also `CompletionItemLabelDetails`) when - * receiving a completion item in a resolve call. - * - * @since 3.17.0 - proposed state - */ - LabelDetailsSupport bool `json:"labelDetailsSupport,omitempty"` - } `json:"completionItem,omitempty"` - WorkDoneProgressOptions -} - -/** - * Completion parameters - */ -type CompletionParams struct { - /** - * The completion context. This is only available it the client specifies - * to send this using the client capability `textDocument.completion.contextSupport === true` - */ - Context CompletionContext `json:"context,omitempty"` - TextDocumentPositionParams - WorkDoneProgressParams - PartialResultParams -} - -/** - * How a completion was triggered - */ -type CompletionTriggerKind float64 - -type ConfigurationClientCapabilities struct { - /** - * The workspace client capabilities - */ - Workspace Workspace4Gn `json:"workspace,omitempty"` -} - -type ConfigurationItem struct { - /** - * The scope to get the configuration section for. - */ - ScopeURI string `json:"scopeUri,omitempty"` - /** - * The configuration section asked for. - */ - Section string `json:"section,omitempty"` -} - -/** - * The parameters of a configuration request. - */ -type ConfigurationParams struct { - Items []ConfigurationItem `json:"items"` -} - -/** - * Create file operation. - */ -type CreateFile struct { - /** - * A create - */ - Kind string `json:"kind"` - /** - * The resource to create. - */ - URI DocumentURI `json:"uri"` - /** - * Additional options - */ - Options CreateFileOptions `json:"options,omitempty"` - ResourceOperation -} - -/** - * Options to create a file. - */ -type CreateFileOptions struct { - /** - * Overwrite existing file. Overwrite wins over `ignoreIfExists` - */ - Overwrite bool `json:"overwrite,omitempty"` - /** - * Ignore if exists. - */ - IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` -} - -/** - * The parameters sent in file create requests/notifications. - * - * @since 3.16.0 - */ -type CreateFilesParams struct { - /** - * An array of all files/folders created in this operation. - */ - Files []FileCreate `json:"files"` -} - -/** - * Defines a decimal number. Since decimal numbers are very - * rare in the language server specification we denote the - * exact range with every decimal using the mathematics - * interval notations (e.g. [0, 1] denotes all decimals d with - * 0 <= d <= 1. - */ -type Decimal = float64 - -/** - * The declaration of a symbol representation as one or many [locations](#Location). - */ -type Declaration = []Location /*Location | Location[]*/ - -/** - * @since 3.14.0 - */ -type DeclarationClientCapabilities struct { - /** - * Whether declaration supports dynamic registration. If this is set to `true` - * the client supports the new `DeclarationRegistrationOptions` return value - * for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports additional metadata in the form of declaration links. - */ - LinkSupport bool `json:"linkSupport,omitempty"` -} - -/** - * Information about where a symbol is declared. - * - * Provides additional metadata over normal [location](#Location) declarations, including the range of - * the declaring symbol. - * - * Servers should prefer returning `DeclarationLink` over `Declaration` if supported - * by the client. - */ -type DeclarationLink = LocationLink - -type DeclarationOptions struct { - WorkDoneProgressOptions -} - -type DeclarationParams struct { - TextDocumentPositionParams - WorkDoneProgressParams - PartialResultParams -} - -type DeclarationRegistrationOptions struct { - DeclarationOptions - TextDocumentRegistrationOptions - StaticRegistrationOptions -} - -/** - * The definition of a symbol represented as one or many [locations](#Location). - * For most programming languages there is only one location at which a symbol is - * defined. - * - * Servers should prefer returning `DefinitionLink` over `Definition` if supported - * by the client. - */ -type Definition = []Location /*Location | Location[]*/ - -/** - * Client Capabilities for a [DefinitionRequest](#DefinitionRequest). - */ -type DefinitionClientCapabilities struct { - /** - * Whether definition supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports additional metadata in the form of definition links. - * - * @since 3.14.0 - */ - LinkSupport bool `json:"linkSupport,omitempty"` -} - -/** - * Information about where a symbol is defined. - * - * Provides additional metadata over normal [location](#Location) definitions, including the range of - * the defining symbol - */ -type DefinitionLink = LocationLink - -/** - * Server Capabilities for a [DefinitionRequest](#DefinitionRequest). - */ -type DefinitionOptions struct { - WorkDoneProgressOptions -} - -/** - * Parameters for a [DefinitionRequest](#DefinitionRequest). - */ -type DefinitionParams struct { - TextDocumentPositionParams - WorkDoneProgressParams - PartialResultParams -} - -/** - * Delete file operation - */ -type DeleteFile struct { - /** - * A delete - */ - Kind string `json:"kind"` - /** - * The file to delete. - */ - URI DocumentURI `json:"uri"` - /** - * Delete options. - */ - Options DeleteFileOptions `json:"options,omitempty"` - ResourceOperation -} - -/** - * Delete file options - */ -type DeleteFileOptions struct { - /** - * Delete the content recursively if a folder is denoted. - */ - Recursive bool `json:"recursive,omitempty"` - /** - * Ignore the operation if the file doesn't exist. - */ - IgnoreIfNotExists bool `json:"ignoreIfNotExists,omitempty"` -} - -/** - * The parameters sent in file delete requests/notifications. - * - * @since 3.16.0 - */ -type DeleteFilesParams struct { - /** - * An array of all files/folders deleted in this operation. - */ - Files []FileDelete `json:"files"` -} - -/** - * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects - * are only valid in the scope of a resource. - */ -type Diagnostic struct { - /** - * The range at which the message applies - */ - Range Range `json:"range"` - /** - * The diagnostic's severity. Can be omitted. If omitted it is up to the - * client to interpret diagnostics as error, warning, info or hint. - */ - Severity DiagnosticSeverity `json:"severity,omitempty"` - /** - * The diagnostic's code, which usually appear in the user interface. - */ - Code interface{}/*integer | string*/ `json:"code,omitempty"` - /** - * An optional property to describe the error code. - * Requires the code field (above) to be present/not null. - * - * @since 3.16.0 - */ - CodeDescription *CodeDescription `json:"codeDescription,omitempty"` - /** - * A human-readable string describing the source of this - * diagnostic, e.g. 'typescript' or 'super lint'. It usually - * appears in the user interface. - */ - Source string `json:"source,omitempty"` - /** - * The diagnostic's message. It usually appears in the user interface - */ - Message string `json:"message"` - /** - * Additional metadata about the diagnostic. - * - * @since 3.15.0 - */ - Tags []DiagnosticTag `json:"tags,omitempty"` - /** - * An array of related diagnostic information, e.g. when symbol-names within - * a scope collide all definitions can be marked via this property. - */ - RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"` - /** - * A data entry field that is preserved between a `textDocument/publishDiagnostics` - * notification and `textDocument/codeAction` request. - * - * @since 3.16.0 - */ - Data LSPAny `json:"data,omitempty"` -} - -/** - * Represents a related message and source code location for a diagnostic. This should be - * used to point to code locations that cause or related to a diagnostics, e.g when duplicating - * a symbol in a scope. - */ -type DiagnosticRelatedInformation struct { - /** - * The location of this related diagnostic information. - */ - Location Location `json:"location"` - /** - * The message of this related diagnostic information. - */ - Message string `json:"message"` -} - -/** - * The diagnostic's severity. - */ -type DiagnosticSeverity float64 - -/** - * The diagnostic tags. - * - * @since 3.15.0 - */ -type DiagnosticTag float64 - -type DidChangeConfigurationClientCapabilities struct { - /** - * Did change configuration notification supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * The parameters of a change configuration notification. - */ -type DidChangeConfigurationParams struct { - /** - * The actual changed settings - */ - Settings LSPAny `json:"settings"` -} - -/** - * The params sent in a change notebook document notification. - * - * @since 3.17.0 - proposed state - */ -type DidChangeNotebookDocumentParams = struct { - /** - * The notebook document that did change. The version number points - * to the version after all provided changes have been applied. If - * only the text document content of a cell changes the notebook version - * doesn't necessarily have to change. - */ - NotebookDocument VersionedNotebookDocumentIdentifier `json:"notebookDocument"` - /** - * The actual changes to the notebook document. - * - * The changes describe single state changes to the notebook document. - * So if there are two changes c1 (at array index 0) and c2 (at array - * index 1) for a notebook in state S then c1 moves the notebook from - * S to S' and c2 from S' to S''. So c1 is computed on the state S and - * c2 is computed on the state S'. - * - * To mirror the content of a notebook using change events use the following approach: - * - start with the same initial content - * - apply the 'notebookDocument/didChange' notifications in the order you receive them. - * - apply the `NotebookChangeEvent`s in a single notification in the order - * you receive them. - */ - Change NotebookDocumentChangeEvent `json:"change"` -} - -/** - * The change text document notification's parameters. - */ -type DidChangeTextDocumentParams struct { - /** - * The document that did change. The version number points - * to the version after all provided content changes have - * been applied. - */ - TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` - /** - * The actual content changes. The content changes describe single state changes - * to the document. So if there are two content changes c1 (at array index 0) and - * c2 (at array index 1) for a document in state S then c1 moves the document from - * S to S' and c2 from S' to S''. So c1 is computed on the state S and c2 is computed - * on the state S'. - * - * To mirror the content of a document using change events use the following approach: - * - start with the same initial content - * - apply the 'textDocument/didChange' notifications in the order you receive them. - * - apply the `TextDocumentContentChangeEvent`s in a single notification in the order - * you receive them. - */ - ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` -} - -type DidChangeWatchedFilesClientCapabilities struct { - /** - * Did change watched files notification supports dynamic registration. Please note - * that the current protocol doesn't support static configuration for file changes - * from the server side. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * The watched files change notification's parameters. - */ -type DidChangeWatchedFilesParams struct { - /** - * The actual file events. - */ - Changes []FileEvent `json:"changes"` -} - -/** - * Describe options to be used when registered for text document change events. - */ -type DidChangeWatchedFilesRegistrationOptions struct { - /** - * The watchers to register. - */ - Watchers []FileSystemWatcher `json:"watchers"` -} - -/** - * The parameters of a `workspace/didChangeWorkspaceFolders` notification. - */ -type DidChangeWorkspaceFoldersParams struct { - /** - * The actual workspace folder change event. - */ - Event WorkspaceFoldersChangeEvent `json:"event"` -} - -/** - * The params sent in a close notebook document notification. - * - * @since 3.17.0 - proposed state - */ -type DidCloseNotebookDocumentParams = struct { - /** - * The notebook document that got closed. - */ - NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` - /** - * The text documents that represent the content - * of a notebook cell that got closed. - */ - CellTextDocuments []TextDocumentIdentifier `json:"cellTextDocuments"` -} - -/** - * The parameters send in a close text document notification - */ -type DidCloseTextDocumentParams struct { - /** - * The document that was closed. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` -} - -/** - * The params sent in a open notebook document notification. - * - * @since 3.17.0 - proposed state - */ -type DidOpenNotebookDocumentParams = struct { - /** - * The notebook document that got opened. - */ - NotebookDocument NotebookDocument `json:"notebookDocument"` - /** - * The text documents that represent the content - * of a notebook cell. - */ - CellTextDocuments []TextDocumentItem `json:"cellTextDocuments"` -} - -/** - * The parameters send in a open text document notification - */ -type DidOpenTextDocumentParams struct { - /** - * The document that was opened. - */ - TextDocument TextDocumentItem `json:"textDocument"` -} - -/** - * The params sent in a save notebook document notification. - * - * @since 3.17.0 - proposed state - */ -type DidSaveNotebookDocumentParams = struct { - /** - * The notebook document that got saved. - */ - NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` -} - -/** - * The parameters send in a save text document notification - */ -type DidSaveTextDocumentParams struct { - /** - * The document that was closed. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * Optional the content when saved. Depends on the includeText value - * when the save notification was requested. - */ - Text *string `json:"text,omitempty"` -} - -type DocumentColorClientCapabilities struct { - /** - * Whether implementation supports dynamic registration. If this is set to `true` - * the client supports the new `DocumentColorRegistrationOptions` return value - * for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -type DocumentColorOptions struct { - WorkDoneProgressOptions -} - -/** - * Parameters for a [DocumentColorRequest](#DocumentColorRequest). - */ -type DocumentColorParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - WorkDoneProgressParams - PartialResultParams -} - -type DocumentColorRegistrationOptions struct { - TextDocumentRegistrationOptions - StaticRegistrationOptions - DocumentColorOptions -} - -/** - * Parameters of the document diagnostic request. - * - * @since 3.17.0 - proposed state - */ -type DocumentDiagnosticParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The additional identifier provided during registration. - */ - Identifier string `json:"identifier,omitempty"` - /** - * The result id of a previous response if provided. - */ - PreviousResultID string `json:"previousResultId,omitempty"` -} - -/** - * The result of a document diagnostic pull request. A report can - * either be a full report containing all diagnostics for the - * requested document or a unchanged report indicating that nothing - * has changed in terms of diagnostics in comparison to the last - * pull request. - * - * @since 3.17.0 - proposed state - */ -type DocumentDiagnosticReport = interface{} /*RelatedFullDocumentDiagnosticReport | RelatedUnchangedDocumentDiagnosticReport*/ - -/** - * A document filter describes a top level text document or - * a notebook cell document. - * - * @since 3.17.0 - proposed support for NotebookCellTextDocumentFilter. - */ -type DocumentFilter = interface{} /*TextDocumentFilter | NotebookCellTextDocumentFilter*/ - -/** - * Client capabilities of a [DocumentFormattingRequest](#DocumentFormattingRequest). - */ -type DocumentFormattingClientCapabilities struct { - /** - * Whether formatting supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * Provider options for a [DocumentFormattingRequest](#DocumentFormattingRequest). - */ -type DocumentFormattingOptions struct { - WorkDoneProgressOptions -} - -/** - * The parameters of a [DocumentFormattingRequest](#DocumentFormattingRequest). - */ -type DocumentFormattingParams struct { - /** - * The document to format. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The format options - */ - Options FormattingOptions `json:"options"` - WorkDoneProgressParams -} - -/** - * A document highlight is a range inside a text document which deserves - * special attention. Usually a document highlight is visualized by changing - * the background color of its range. - */ -type DocumentHighlight struct { - /** - * The range this highlight applies to. - */ - Range Range `json:"range"` - /** - * The highlight kind, default is [text](#DocumentHighlightKind.Text). - */ - Kind DocumentHighlightKind `json:"kind,omitempty"` -} - -/** - * Client Capabilities for a [DocumentHighlightRequest](#DocumentHighlightRequest). - */ -type DocumentHighlightClientCapabilities struct { - /** - * Whether document highlight supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * A document highlight kind. - */ -type DocumentHighlightKind float64 - -/** - * Provider options for a [DocumentHighlightRequest](#DocumentHighlightRequest). - */ -type DocumentHighlightOptions struct { - WorkDoneProgressOptions -} - -/** - * Parameters for a [DocumentHighlightRequest](#DocumentHighlightRequest). - */ -type DocumentHighlightParams struct { - TextDocumentPositionParams - WorkDoneProgressParams - PartialResultParams -} - -/** - * A document link is a range in a text document that links to an internal or external resource, like another - * text document or a web site. - */ -type DocumentLink struct { - /** - * The range this link applies to. - */ - Range Range `json:"range"` - /** - * The uri this link points to. - */ - Target string `json:"target,omitempty"` - /** - * The tooltip text when you hover over this link. - * - * If a tooltip is provided, is will be displayed in a string that includes instructions on how to - * trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS, - * user settings, and localization. - * - * @since 3.15.0 - */ - Tooltip string `json:"tooltip,omitempty"` - /** - * A data entry field that is preserved on a document link between a - * DocumentLinkRequest and a DocumentLinkResolveRequest. - */ - Data LSPAny `json:"data,omitempty"` -} - -/** - * The client capabilities of a [DocumentLinkRequest](#DocumentLinkRequest). - */ -type DocumentLinkClientCapabilities struct { - /** - * Whether document link supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Whether the client support the `tooltip` property on `DocumentLink`. - * - * @since 3.15.0 - */ - TooltipSupport bool `json:"tooltipSupport,omitempty"` -} - -/** - * Provider options for a [DocumentLinkRequest](#DocumentLinkRequest). - */ -type DocumentLinkOptions struct { - /** - * Document links have a resolve provider as well. - */ - ResolveProvider bool `json:"resolveProvider,omitempty"` - WorkDoneProgressOptions -} - -/** - * The parameters of a [DocumentLinkRequest](#DocumentLinkRequest). - */ -type DocumentLinkParams struct { - /** - * The document to provide document links for. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * Client capabilities of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). - */ -type DocumentOnTypeFormattingClientCapabilities struct { - /** - * Whether on type formatting supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * Provider options for a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). - */ -type DocumentOnTypeFormattingOptions struct { - /** - * A character on which formatting should be triggered, like `}`. - */ - FirstTriggerCharacter string `json:"firstTriggerCharacter"` - /** - * More trigger characters. - */ - MoreTriggerCharacter []string `json:"moreTriggerCharacter,omitempty"` -} - -/** - * The parameters of a [DocumentOnTypeFormattingRequest](#DocumentOnTypeFormattingRequest). - */ -type DocumentOnTypeFormattingParams struct { - /** - * The document to format. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The position at which this request was send. - */ - Position Position `json:"position"` - /** - * The character that has been typed. - */ - Ch string `json:"ch"` - /** - * The format options. - */ - Options FormattingOptions `json:"options"` -} - -/** - * Client capabilities of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). - */ -type DocumentRangeFormattingClientCapabilities struct { - /** - * Whether range formatting supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * Provider options for a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). - */ -type DocumentRangeFormattingOptions struct { - WorkDoneProgressOptions -} - -/** - * The parameters of a [DocumentRangeFormattingRequest](#DocumentRangeFormattingRequest). - */ -type DocumentRangeFormattingParams struct { - /** - * The document to format. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The range to format - */ - Range Range `json:"range"` - /** - * The format options - */ - Options FormattingOptions `json:"options"` - WorkDoneProgressParams -} - -/** - * A document selector is the combination of one or many document filters. - * - * @sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**∕tsconfig.json' }]`; - * - * The use of a string as a document filter is deprecated @since 3.16.0. - */ -type DocumentSelector = []string /*string | DocumentFilter*/ - -/** - * Represents programming constructs like variables, classes, interfaces etc. - * that appear in a document. Document symbols can be hierarchical and they - * have two ranges: one that encloses its definition and one that points to - * its most interesting range, e.g. the range of an identifier. - */ -type DocumentSymbol struct { - /** - * The name of this symbol. Will be displayed in the user interface and therefore must not be - * an empty string or a string only consisting of white spaces. - */ - Name string `json:"name"` - /** - * More detail for this symbol, e.g the signature of a function. - */ - Detail string `json:"detail,omitempty"` - /** - * The kind of this symbol. - */ - Kind SymbolKind `json:"kind"` - /** - * Tags for this document symbol. - * - * @since 3.16.0 - */ - Tags []SymbolTag `json:"tags,omitempty"` - /** - * Indicates if this symbol is deprecated. - * - * @deprecated Use tags instead - */ - Deprecated bool `json:"deprecated,omitempty"` - /** - * The range enclosing this symbol not including leading/trailing whitespace but everything else - * like comments. This information is typically used to determine if the the clients cursor is - * inside the symbol to reveal in the symbol in the UI. - */ - Range Range `json:"range"` - /** - * The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. - * Must be contained by the the `range`. - */ - SelectionRange Range `json:"selectionRange"` - /** - * Children of this symbol, e.g. properties of a class. - */ - Children []DocumentSymbol `json:"children,omitempty"` -} - -/** - * Client Capabilities for a [DocumentSymbolRequest](#DocumentSymbolRequest). - */ -type DocumentSymbolClientCapabilities struct { - /** - * Whether document symbol supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Specific capabilities for the `SymbolKind`. - */ - SymbolKind struct { - /** - * The symbol kind values the client supports. When this - * property exists the client also guarantees that it will - * handle values outside its set gracefully and falls back - * to a default value when unknown. - * - * If this property is not present the client only supports - * the symbol kinds from `File` to `Array` as defined in - * the initial version of the protocol. - */ - ValueSet []SymbolKind `json:"valueSet,omitempty"` - } `json:"symbolKind,omitempty"` - /** - * The client support hierarchical document symbols. - */ - HierarchicalDocumentSymbolSupport bool `json:"hierarchicalDocumentSymbolSupport,omitempty"` - /** - * The client supports tags on `SymbolInformation`. Tags are supported on - * `DocumentSymbol` if `hierarchicalDocumentSymbolSupport` is set to true. - * Clients supporting tags have to handle unknown tags gracefully. - * - * @since 3.16.0 - */ - TagSupport struct { - /** - * The tags supported by the client. - */ - ValueSet []SymbolTag `json:"valueSet"` - } `json:"tagSupport,omitempty"` - /** - * The client supports an additional label presented in the UI when - * registering a document symbol provider. - * - * @since 3.16.0 - */ - LabelSupport bool `json:"labelSupport,omitempty"` -} - -/** - * Provider options for a [DocumentSymbolRequest](#DocumentSymbolRequest). - */ -type DocumentSymbolOptions struct { - /** - * A human-readable string that is shown when multiple outlines trees - * are shown for the same document. - * - * @since 3.16.0 - */ - Label string `json:"label,omitempty"` - WorkDoneProgressOptions -} - -/** - * Parameters for a [DocumentSymbolRequest](#DocumentSymbolRequest). - */ -type DocumentSymbolParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * A tagging type for string properties that are actually document URIs. - */ -type DocumentURI string - -/** - * The client capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). - */ -type ExecuteCommandClientCapabilities struct { - /** - * Execute command supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * The server capabilities of a [ExecuteCommandRequest](#ExecuteCommandRequest). - */ -type ExecuteCommandOptions struct { - /** - * The commands to be executed on the server - */ - Commands []string `json:"commands"` - WorkDoneProgressOptions -} - -/** - * The parameters of a [ExecuteCommandRequest](#ExecuteCommandRequest). - */ -type ExecuteCommandParams struct { - /** - * The identifier of the actual command handler. - */ - Command string `json:"command"` - /** - * Arguments that the command should be invoked with. - */ - Arguments []json.RawMessage `json:"arguments,omitempty"` - WorkDoneProgressParams -} - -type ExecutionSummary = struct { - /** - * A strict monotonically increasing value - * indicating the execution order of a cell - * inside a notebook. - */ - ExecutionOrder uint32 `json:"executionOrder"` - /** - * Whether the execution was successful or - * not if known by the client. - */ - Success bool `json:"success,omitempty"` -} - -type FailureHandlingKind string - -/** - * The file event type - */ -type FileChangeType float64 - -/** - * Represents information on a file/folder create. - * - * @since 3.16.0 - */ -type FileCreate struct { - /** - * A file:// URI for the location of the file/folder being created. - */ - URI string `json:"uri"` -} - -/** - * Represents information on a file/folder delete. - * - * @since 3.16.0 - */ -type FileDelete struct { - /** - * A file:// URI for the location of the file/folder being deleted. - */ - URI string `json:"uri"` -} - -/** - * An event describing a file change. - */ -type FileEvent struct { - /** - * The file's uri. - */ - URI DocumentURI `json:"uri"` - /** - * The change type. - */ - Type FileChangeType `json:"type"` -} - -/** - * Capabilities relating to events from file operations by the user in the client. - * - * These events do not come from the file system, they come from user operations - * like renaming a file in the UI. - * - * @since 3.16.0 - */ -type FileOperationClientCapabilities struct { - /** - * Whether the client supports dynamic registration for file requests/notifications. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client has support for sending didCreateFiles notifications. - */ - DidCreate bool `json:"didCreate,omitempty"` - /** - * The client has support for willCreateFiles requests. - */ - WillCreate bool `json:"willCreate,omitempty"` - /** - * The client has support for sending didRenameFiles notifications. - */ - DidRename bool `json:"didRename,omitempty"` - /** - * The client has support for willRenameFiles requests. - */ - WillRename bool `json:"willRename,omitempty"` - /** - * The client has support for sending didDeleteFiles notifications. - */ - DidDelete bool `json:"didDelete,omitempty"` - /** - * The client has support for willDeleteFiles requests. - */ - WillDelete bool `json:"willDelete,omitempty"` -} - -/** - * A filter to describe in which file operation requests or notifications - * the server is interested in. - * - * @since 3.16.0 - */ -type FileOperationFilter struct { - /** - * A Uri like `file` or `untitled`. - */ - Scheme string `json:"scheme,omitempty"` - /** - * The actual file operation pattern. - */ - Pattern FileOperationPattern `json:"pattern"` -} - -/** - * Options for notifications/requests for user operations on files. - * - * @since 3.16.0 - */ -type FileOperationOptions struct { - /** - * The server is interested in didCreateFiles notifications. - */ - DidCreate FileOperationRegistrationOptions `json:"didCreate,omitempty"` - /** - * The server is interested in willCreateFiles requests. - */ - WillCreate FileOperationRegistrationOptions `json:"willCreate,omitempty"` - /** - * The server is interested in didRenameFiles notifications. - */ - DidRename FileOperationRegistrationOptions `json:"didRename,omitempty"` - /** - * The server is interested in willRenameFiles requests. - */ - WillRename FileOperationRegistrationOptions `json:"willRename,omitempty"` - /** - * The server is interested in didDeleteFiles file notifications. - */ - DidDelete FileOperationRegistrationOptions `json:"didDelete,omitempty"` - /** - * The server is interested in willDeleteFiles file requests. - */ - WillDelete FileOperationRegistrationOptions `json:"willDelete,omitempty"` -} - -/** - * A pattern to describe in which file operation requests or notifications - * the server is interested in. - * - * @since 3.16.0 - */ -type FileOperationPattern struct { - /** - * The glob pattern to match. Glob patterns can have the following syntax: - * - `*` to match one or more characters in a path segment - * - `?` to match on one character in a path segment - * - `**` to match any number of path segments, including none - * - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) - * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) - */ - Glob string `json:"glob"` - /** - * Whether to match files or folders with this pattern. - * - * Matches both if undefined. - */ - Matches FileOperationPatternKind `json:"matches,omitempty"` - /** - * Additional options used during matching. - */ - Options FileOperationPatternOptions `json:"options,omitempty"` -} - -/** - * A pattern kind describing if a glob pattern matches a file a folder or - * both. - * - * @since 3.16.0 - */ -type FileOperationPatternKind string - -/** - * Matching options for the file operation pattern. - * - * @since 3.16.0 - */ -type FileOperationPatternOptions struct { - /** - * The pattern should be matched ignoring casing. - */ - IgnoreCase bool `json:"ignoreCase,omitempty"` -} - -/** - * The options to register for file operations. - * - * @since 3.16.0 - */ -type FileOperationRegistrationOptions struct { - /** - * The actual filters. - */ - Filters []FileOperationFilter `json:"filters"` -} - -/** - * Represents information on a file/folder rename. - * - * @since 3.16.0 - */ -type FileRename struct { - /** - * A file:// URI for the original location of the file/folder being renamed. - */ - OldURI string `json:"oldUri"` - /** - * A file:// URI for the new location of the file/folder being renamed. - */ - NewURI string `json:"newUri"` -} - -type FileSystemWatcher struct { - /** - * The glob pattern to watch. Glob patterns can have the following syntax: - * - `*` to match one or more characters in a path segment - * - `?` to match on one character in a path segment - * - `**` to match any number of path segments, including none - * - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) - * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) - */ - GlobPattern string `json:"globPattern"` - /** - * The kind of events of interest. If omitted it defaults - * to WatchKind.Create | WatchKind.Change | WatchKind.Delete - * which is 7. - */ - Kind uint32 `json:"kind,omitempty"` -} - -/** - * Represents a folding range. To be valid, start and end line must be bigger than zero and smaller - * than the number of lines in the document. Clients are free to ignore invalid ranges. - */ -type FoldingRange struct { - /** - * The zero-based start line of the range to fold. The folded area starts after the line's last character. - * To be valid, the end must be zero or larger and smaller than the number of lines in the document. - */ - StartLine uint32 `json:"startLine"` - /** - * The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. - */ - StartCharacter uint32 `json:"startCharacter,omitempty"` - /** - * The zero-based end line of the range to fold. The folded area ends with the line's last character. - * To be valid, the end must be zero or larger and smaller than the number of lines in the document. - */ - EndLine uint32 `json:"endLine"` - /** - * The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. - */ - EndCharacter uint32 `json:"endCharacter,omitempty"` - /** - * Describes the kind of the folding range such as `comment' or 'region'. The kind - * is used to categorize folding ranges and used by commands like 'Fold all comments'. See - * [FoldingRangeKind](#FoldingRangeKind) for an enumeration of standardized kinds. - */ - Kind string `json:"kind,omitempty"` -} - -type FoldingRangeClientCapabilities struct { - /** - * Whether implementation supports dynamic registration for folding range providers. If this is set to `true` - * the client supports the new `FoldingRangeRegistrationOptions` return value for the corresponding server - * capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The maximum number of folding ranges that the client prefers to receive per document. The value serves as a - * hint, servers are free to follow the limit. - */ - RangeLimit uint32 `json:"rangeLimit,omitempty"` - /** - * If set, the client signals that it only supports folding complete lines. If set, client will - * ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange. - */ - LineFoldingOnly bool `json:"lineFoldingOnly,omitempty"` -} - -/** - * Enum of known range kinds - */ -type FoldingRangeKind string - -type FoldingRangeOptions struct { - WorkDoneProgressOptions -} - -/** - * Parameters for a [FoldingRangeRequest](#FoldingRangeRequest). - */ -type FoldingRangeParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - WorkDoneProgressParams - PartialResultParams -} - -type FoldingRangeRegistrationOptions struct { - TextDocumentRegistrationOptions - FoldingRangeOptions - StaticRegistrationOptions -} - -/** - * Value-object describing what options formatting should use. - */ -type FormattingOptions struct { - /** - * Size of a tab in spaces. - */ - TabSize uint32 `json:"tabSize"` - /** - * Prefer spaces over tabs. - */ - InsertSpaces bool `json:"insertSpaces"` - /** - * Trim trailing whitespaces on a line. - * - * @since 3.15.0 - */ - TrimTrailingWhitespace bool `json:"trimTrailingWhitespace,omitempty"` - /** - * Insert a newline character at the end of the file if one does not exist. - * - * @since 3.15.0 - */ - InsertFinalNewline bool `json:"insertFinalNewline,omitempty"` - /** - * Trim all newlines after the final newline at the end of the file. - * - * @since 3.15.0 - */ - TrimFinalNewlines bool `json:"trimFinalNewlines,omitempty"` -} - -/** - * A diagnostic report with a full set of problems. - * - * @since 3.17.0 - proposed state - */ -type FullDocumentDiagnosticReport = struct { - /** - * A full document diagnostic report. - */ - Kind string `json:"kind"` - /** - * An optional result id. If provided it will - * be sent on the next diagnostic request for the - * same document. - */ - ResultID string `json:"resultId,omitempty"` - /** - * The actual items. - */ - Items []Diagnostic `json:"items"` -} - -/** - * General client capabilities. - * - * @since 3.16.0 - */ -type GeneralClientCapabilities struct { - /** - * Client capability that signals how the client - * handles stale requests (e.g. a request - * for which the client will not process the response - * anymore since the information is outdated). - * - * @since 3.17.0 - */ - StaleRequestSupport struct { - /** - * The client will actively cancel the request. - */ - Cancel bool `json:"cancel"` - /** - * The list of requests for which the client - * will retry the request if it receives a - * response with error code `ContentModified` - */ - RetryOnContentModified []string `json:"retryOnContentModified"` - } `json:"staleRequestSupport,omitempty"` - /** - * Client capabilities specific to regular expressions. - * - * @since 3.16.0 - */ - RegularExpressions RegularExpressionsClientCapabilities `json:"regularExpressions,omitempty"` - /** - * Client capabilities specific to the client's markdown parser. - * - * @since 3.16.0 - */ - Markdown MarkdownClientCapabilities `json:"markdown,omitempty"` -} - -/** - * The result of a hover request. - */ -type Hover struct { - /** - * The hover's content - */ - Contents MarkupContent/*MarkupContent | MarkedString | MarkedString[]*/ `json:"contents"` - /** - * An optional range - */ - Range Range `json:"range,omitempty"` -} - -type HoverClientCapabilities struct { - /** - * Whether hover supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Client supports the follow content formats for the content - * property. The order describes the preferred format of the client. - */ - ContentFormat []MarkupKind `json:"contentFormat,omitempty"` -} - -/** - * Hover options. - */ -type HoverOptions struct { - WorkDoneProgressOptions -} - -/** - * Parameters for a [HoverRequest](#HoverRequest). - */ -type HoverParams struct { - TextDocumentPositionParams - WorkDoneProgressParams -} - -/** - * @since 3.6.0 - */ -type ImplementationClientCapabilities struct { - /** - * Whether implementation supports dynamic registration. If this is set to `true` - * the client supports the new `ImplementationRegistrationOptions` return value - * for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports additional metadata in the form of definition links. - * - * @since 3.14.0 - */ - LinkSupport bool `json:"linkSupport,omitempty"` -} - -type ImplementationOptions struct { - WorkDoneProgressOptions -} - -type ImplementationParams struct { - TextDocumentPositionParams - WorkDoneProgressParams - PartialResultParams -} - -type ImplementationRegistrationOptions struct { - TextDocumentRegistrationOptions - ImplementationOptions - StaticRegistrationOptions -} - -/** - * Known error codes for an `InitializeError`; - */ -type InitializeError float64 - -type InitializeParams struct { - /** - * The process Id of the parent process that started - * the server. - */ - ProcessID int32/*integer | null*/ `json:"processId"` - /** - * Information about the client - * - * @since 3.15.0 - */ - ClientInfo struct { - /** - * The name of the client as defined by the client. - */ - Name string `json:"name"` - /** - * The client's version as defined by the client. - */ - Version string `json:"version,omitempty"` - } `json:"clientInfo,omitempty"` - /** - * The locale the client is currently showing the user interface - * in. This must not necessarily be the locale of the operating - * system. - * - * Uses IETF language tags as the value's syntax - * (See https://en.wikipedia.org/wiki/IETF_language_tag) - * - * @since 3.16.0 - */ - Locale string `json:"locale,omitempty"` - /** - * The rootPath of the workspace. Is null - * if no folder is open. - * - * @deprecated in favour of rootUri. - */ - RootPath string/*string | null*/ `json:"rootPath,omitempty"` - /** - * The rootUri of the workspace. Is null if no - * folder is open. If both `rootPath` and `rootUri` are set - * `rootUri` wins. - * - * @deprecated in favour of workspaceFolders. - */ - RootURI DocumentURI/*DocumentUri | null*/ `json:"rootUri"` - /** - * The capabilities provided by the client (editor or tool) - */ - Capabilities ClientCapabilities `json:"capabilities"` - /** - * User provided initialization options. - */ - InitializationOptions LSPAny `json:"initializationOptions,omitempty"` - /** - * The initial trace setting. If omitted trace is disabled ('off'). - */ - Trace string/* 'off' | 'messages' | 'compact' | 'verbose' */ `json:"trace,omitempty"` - /** - * The actual configured workspace folders. - */ - WorkspaceFolders []WorkspaceFolder/*WorkspaceFolder[] | null*/ `json:"workspaceFolders"` -} - -/** - * The result returned from an initialize request. - */ -type InitializeResult struct { - /** - * The capabilities the language server provides. - */ - Capabilities ServerCapabilities `json:"capabilities"` - /** - * Information about the server. - * - * @since 3.15.0 - */ - ServerInfo struct { - /** - * The name of the server as defined by the server. - */ - Name string `json:"name"` - /** - * The server's version as defined by the server. - */ - Version string `json:"version,omitempty"` - } `json:"serverInfo,omitempty"` -} - -type InitializedParams struct { -} - -/** - * Inlay hint information. - * - * @since 3.17.0 - proposed state - */ -type InlayHint = struct { - /** - * The position of this hint. - */ - Position *Position `json:"position"` - /** - * The label of this hint. A human readable string or an array of - * InlayHintLabelPart label parts. - * - * *Note* that neither the string nor the label part can be empty. - */ - Label []InlayHintLabelPart/*string | InlayHintLabelPart[]*/ `json:"label"` - /** - * The kind of this hint. Can be omitted in which case the client - * should fall back to a reasonable default. - */ - Kind InlayHintKind `json:"kind,omitempty"` - /** - * The tooltip text when you hover over this item. - */ - Tooltip string/*string | MarkupContent*/ `json:"tooltip,omitempty"` - /** - * Render padding before the hint. - * - * Note: Padding should use the editor's background color, not the - * background color of the hint itself. That means padding can be used - * to visually align/separate an inlay hint. - */ - PaddingLeft bool `json:"paddingLeft,omitempty"` - /** - * Render padding after the hint. - * - * Note: Padding should use the editor's background color, not the - * background color of the hint itself. That means padding can be used - * to visually align/separate an inlay hint. - */ - PaddingRight bool `json:"paddingRight,omitempty"` -} - -/** - * Inlay hint client capabilities - * - * @since 3.17.0 - proposed state - */ -type InlayHintClientCapabilities = struct { - /** - * Whether inlay hints support dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Indicates which properties a client can resolve lazily on a inlay - * hint. - */ - ResolveSupport struct { - /** - * The properties that a client can resolve lazily. - */ - Properties []string `json:"properties"` - } `json:"resolveSupport,omitempty"` -} - -/** - * Inlay hint kinds. - * - * @since 3.17.0 - proposed state - */ -type InlayHintKind float64 - -/** - * An inlay hint label part allows for interactive and composite labels - * of inlay hints. - * - * @since 3.17.0 - proposed state - */ -type InlayHintLabelPart = struct { - /** - * The value of this label part. - */ - Value string `json:"value"` - /** - * The tooltip text when you hover over this label part. Depending on - * the client capability `inlayHint.resolveSupport` clients might resolve - * this property late using the resolve request. - */ - Tooltip string/*string | MarkupContent*/ `json:"tooltip,omitempty"` - /** - * An optional source code location that represents this - * label part. - * - * The editor will use this location for the hover and for code navigation - * features: This part will become a clickable link that resolves to the - * definition of the symbol at the given location (not necessarily the - * location itself), it shows the hover that shows at the given location, - * and it shows a context menu with further code navigation commands. - * - * Depending on the client capability `inlayHint.resolveSupport` clients - * might resolve this property late using the resolve request. - */ - Location *Location `json:"location,omitempty"` - /** - * An optional command for this label part. - * - * Depending on the client capability `inlayHint.resolveSupport` clients - * might resolve this property late using the resolve request. - */ - Command *Command `json:"command,omitempty"` -} - -/** - * Inlay hint options used during static registration. - * - * @since 3.17.0 - proposed state - */ -type InlayHintOptions struct { - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` - /** - * The server provides support to resolve additional - * information for an inlay hint item. - */ - ResolveProvider bool `json:"resolveProvider,omitempty"` -} - -/** - * A parameter literal used in inlay hints requests. - * - * @since 3.17.0 - proposed state - */ -type InlayHintParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The visible document range for which inlay hints should be computed. - */ - ViewPort Range `json:"viewPort"` -} - -/** - * Inlay hint options used during static or dynamic registration. - * - * @since 3.17.0 - proposed state - */ -type InlayHintRegistrationOptions struct { - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` - /** - * The server provides support to resolve additional - * information for an inlay hint item. - */ - ResolveProvider bool `json:"resolveProvider,omitempty"` - /** - * A document selector to identify the scope of the registration. If set to null - * the document selector provided on the client side will be used. - */ - DocumentSelector DocumentSelector/*DocumentSelector | null*/ `json:"documentSelector"` - /** - * The id used to register the request. The id can be used to deregister - * the request again. See also Registration#id. - */ - ID string `json:"id,omitempty"` -} - -/** - * Client workspace capabilities specific to inlay hints. - * - * @since 3.17.0 - proposed state - */ -type InlayHintWorkspaceClientCapabilities = struct { - /** - * Whether the client implementation supports a refresh request sent from - * the server to the client. - * - * Note that this event is global and will force the client to refresh all - * inlay hints currently shown. It should be used with absolute care and - * is useful for situation where a server for example detects a project wide - * change that requires such a calculation. - */ - RefreshSupport bool `json:"refreshSupport,omitempty"` -} - -/** - * Inline value information can be provided by different means: - * - directly as a text value (class InlineValueText). - * - as a name to use for a variable lookup (class InlineValueVariableLookup) - * - as an evaluatable expression (class InlineValueEvaluatableExpression) - * The InlineValue types combines all inline value types into one type. - * - * @since 3.17.0 - proposed state - */ -type InlineValue = interface{} /* InlineValueText | InlineValueVariableLookup | InlineValueEvaluatableExpression*/ - -/** - * Client capabilities specific to inline values. - * - * @since 3.17.0 - proposed state - */ -type InlineValueClientCapabilities = struct { - /** - * Whether implementation supports dynamic registration for inline value providers. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * @since 3.17.0 - proposed state - */ -type InlineValueContext = struct { - /** - * The document range where execution has stopped. - * Typically the end position of the range denotes the line where the inline values are shown. - */ - StoppedLocation *Range `json:"stoppedLocation"` -} - -/** - * Provide an inline value through an expression evaluation. - * If only a range is specified, the expression will be extracted from the underlying document. - * An optional expression can be used to override the extracted expression. - * - * @since 3.17.0 - proposed state - */ -type InlineValueEvaluatableExpression = struct { - /** - * The document range for which the inline value applies. - * The range is used to extract the evaluatable expression from the underlying document. - */ - Range *Range `json:"range"` - /** - * If specified the expression overrides the extracted expression. - */ - Expression string `json:"expression,omitempty"` -} - -/** - * Inline value options used during static registration. - * - * @since 3.17.0 - proposed state - */ -type InlineValueOptions = WorkDoneProgressOptions - -/** - * A parameter literal used in inline value requests. - * - * @since 3.17.0 - proposed state - */ -type InlineValueParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The visible document range for which inline values should be computed. - */ - ViewPort Range `json:"viewPort"` - /** - * Additional information about the context in which inline values were - * requested. - */ - Context InlineValueContext `json:"context"` -} - -/** - * Inline value options used during static or dynamic registration. - * - * @since 3.17.0 - proposed state - */ -type InlineValueRegistrationOptions struct { - /** - * A document selector to identify the scope of the registration. If set to null - * the document selector provided on the client side will be used. - */ - DocumentSelector DocumentSelector/*DocumentSelector | null*/ `json:"documentSelector"` - /** - * The id used to register the request. The id can be used to deregister - * the request again. See also Registration#id. - */ - ID string `json:"id,omitempty"` -} - -/** - * Provide inline value as text. - * - * @since 3.17.0 - proposed state - */ -type InlineValueText = struct { - /** - * The document range for which the inline value applies. - */ - Range *Range `json:"range"` - /** - * The text of the inline value. - */ - Text string `json:"text"` -} - -/** - * Provide inline value through a variable lookup. - * If only a range is specified, the variable name will be extracted from the underlying document. - * An optional variable name can be used to override the extracted name. - * - * @since 3.17.0 - proposed state - */ -type InlineValueVariableLookup = struct { - /** - * The document range for which the inline value applies. - * The range is used to extract the variable name from the underlying document. - */ - Range *Range `json:"range"` - /** - * If specified the name of the variable to look up. - */ - VariableName string `json:"variableName,omitempty"` - /** - * How to perform the lookup. - */ - CaseSensitiveLookup bool `json:"caseSensitiveLookup"` -} - -/** - * Client workspace capabilities specific to inline values. - * - * @since 3.17.0 - proposed state - */ -type InlineValueWorkspaceClientCapabilities = struct { - /** - * Whether the client implementation supports a refresh request sent from the - * server to the client. - * - * Note that this event is global and will force the client to refresh all - * inline values currently shown. It should be used with absolute care and is - * useful for situation where a server for example detects a project wide - * change that requires such a calculation. - */ - RefreshSupport bool `json:"refreshSupport,omitempty"` -} - -/** - * A special text edit to provide an insert and a replace operation. - * - * @since 3.16.0 - */ -type InsertReplaceEdit struct { - /** - * The string to be inserted. - */ - NewText string `json:"newText"` - /** - * The range if the insert is requested - */ - Insert Range `json:"insert"` - /** - * The range if the replace is requested. - */ - Replace Range `json:"replace"` -} - -/** - * Defines whether the insert text in a completion item should be interpreted as - * plain text or a snippet. - */ -type InsertTextFormat float64 - -/** - * How whitespace and indentation is handled during completion - * item insertion. - * - * @since 3.16.0 - */ -type InsertTextMode float64 - -/** - * The LSP any type - * - * @since 3.17.0 - */ -type LSPAny = interface{} /* LSPObject | LSPArray | string | int32 | uint32 | Decimal | bool | float64*/ - -/** - * LSP arrays. - * - * @since 3.17.0 - */ -type LSPArray = []LSPAny - -/** - * LSP object definition. - * - * @since 3.17.0 - */ -type LSPObject = map[string]interface{} /*[key: string]: LSPAny*/ - -/** - * Client capabilities for the linked editing range request. - * - * @since 3.16.0 - */ -type LinkedEditingRangeClientCapabilities struct { - /** - * Whether implementation supports dynamic registration. If this is set to `true` - * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` - * return value for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -type LinkedEditingRangeOptions struct { - WorkDoneProgressOptions -} - -type LinkedEditingRangeParams struct { - TextDocumentPositionParams - WorkDoneProgressParams -} - -type LinkedEditingRangeRegistrationOptions struct { - TextDocumentRegistrationOptions - LinkedEditingRangeOptions - StaticRegistrationOptions -} - -/** - * The result of a linked editing range request. - * - * @since 3.16.0 - */ -type LinkedEditingRanges struct { - /** - * A list of ranges that can be edited together. The ranges must have - * identical length and contain identical text content. The ranges cannot overlap. - */ - Ranges []Range `json:"ranges"` - /** - * An optional word pattern (regular expression) that describes valid contents for - * the given ranges. If no pattern is provided, the client configuration's word - * pattern will be used. - */ - WordPattern string `json:"wordPattern,omitempty"` -} - -/** - * Represents a location inside a resource, such as a line - * inside a text file. - */ -type Location struct { - URI DocumentURI `json:"uri"` - Range Range `json:"range"` -} - -/** - * Represents the connection of two locations. Provides additional metadata over normal [locations](#Location), - * including an origin range. - */ -type LocationLink struct { - /** - * Span of the origin of this link. - * - * Used as the underlined span for mouse definition hover. Defaults to the word range at - * the definition position. - */ - OriginSelectionRange Range `json:"originSelectionRange,omitempty"` - /** - * The target resource identifier of this link. - */ - TargetURI DocumentURI `json:"targetUri"` - /** - * The full target range of this link. If the target for example is a symbol then target range is the - * range enclosing this symbol not including leading/trailing whitespace but everything else - * like comments. This information is typically used to highlight the range in the editor. - */ - TargetRange Range `json:"targetRange"` - /** - * The range that should be selected and revealed when this link is being followed, e.g the name of a function. - * Must be contained by the the `targetRange`. See also `DocumentSymbol#range` - */ - TargetSelectionRange Range `json:"targetSelectionRange"` -} - -/** - * The log message parameters. - */ -type LogMessageParams struct { - /** - * The message type. See {@link MessageType} - */ - Type MessageType `json:"type"` - /** - * The actual message - */ - Message string `json:"message"` -} - -type LogTraceParams struct { - Message string `json:"message"` - Verbose string `json:"verbose,omitempty"` -} - -/** - * Client capabilities specific to the used markdown parser. - * - * @since 3.16.0 - */ -type MarkdownClientCapabilities struct { - /** - * The name of the parser. - */ - Parser string `json:"parser"` - /** - * The version of the parser. - */ - Version string `json:"version,omitempty"` - /** - * A list of HTML tags that the client allows / supports in - * Markdown. - * - * @since 3.17.0 - */ - AllowedTags []string `json:"allowedTags,omitempty"` -} - -/** - * MarkedString can be used to render human readable text. It is either a markdown string - * or a code-block that provides a language and a code snippet. The language identifier - * is semantically equal to the optional language identifier in fenced code blocks in GitHub - * issues. See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting - * - * The pair of a language and a value is an equivalent to markdown: - * ```${language} - * ${value} - * ``` - * - * Note that markdown strings will be sanitized - that means html will be escaped. - * @deprecated use MarkupContent instead. - */ -type MarkedString = string /*string | { language: string; value: string }*/ - -/** - * A `MarkupContent` literal represents a string value which content is interpreted base on its - * kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. - * - * If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. - * See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting - * - * Here is an example how such a string can be constructed using JavaScript / TypeScript: - * ```ts - * let markdown: MarkdownContent = { - * kind: MarkupKind.Markdown, - * value: [ - * '# Header', - * 'Some text', - * '```typescript', - * 'someCode();', - * '```' - * ].join('\n') - * }; - * ``` - * - * *Please Note* that clients might sanitize the return markdown. A client could decide to - * remove HTML from the markdown to avoid script execution. - */ -type MarkupContent struct { - /** - * The type of the Markup - */ - Kind MarkupKind `json:"kind"` - /** - * The content itself - */ - Value string `json:"value"` -} - -/** - * Describes the content type that a client supports in various - * result literals like `Hover`, `ParameterInfo` or `CompletionItem`. - * - * Please note that `MarkupKinds` must not start with a `$`. This kinds - * are reserved for internal usage. - */ -type MarkupKind string - -type MessageActionItem struct { - /** - * A short title like 'Retry', 'Open Log' etc. - */ - Title string `json:"title"` -} - -/** - * The message type - */ -type MessageType float64 - -/** - * Moniker definition to match LSIF 0.5 moniker definition. - * - * @since 3.16.0 - */ -type Moniker struct { - /** - * The scheme of the moniker. For example tsc or .Net - */ - Scheme string `json:"scheme"` - /** - * The identifier of the moniker. The value is opaque in LSIF however - * schema owners are allowed to define the structure if they want. - */ - Identifier string `json:"identifier"` - /** - * The scope in which the moniker is unique - */ - Unique UniquenessLevel `json:"unique"` - /** - * The moniker kind if known. - */ - Kind MonikerKind `json:"kind,omitempty"` -} - -/** - * Client capabilities specific to the moniker request. - * - * @since 3.16.0 - */ -type MonikerClientCapabilities struct { - /** - * Whether moniker supports dynamic registration. If this is set to `true` - * the client supports the new `MonikerRegistrationOptions` return value - * for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * The moniker kind. - * - * @since 3.16.0 - */ -type MonikerKind string - -type MonikerOptions struct { - WorkDoneProgressOptions -} - -type MonikerParams struct { - TextDocumentPositionParams - WorkDoneProgressParams - PartialResultParams -} - -type MonikerRegistrationOptions struct { - TextDocumentRegistrationOptions - MonikerOptions -} - -/** - * A notebook cell. - * - * A cell's document URI must be unique across ALL notebook - * cells and can therefore be used to uniquely identify a - * notebook cell or the cell's text document. - * - * @since 3.17.0 - proposed state - */ -type NotebookCell = struct { - /** - * The cell's kind - */ - Kind NotebookCellKind `json:"kind"` - /** - * The URI of the cell's text document - * content. - */ - Document DocumentURI `json:"document"` - /** - * Additional metadata stored with the cell. - */ - Metadata LSPObject `json:"metadata,omitempty"` - /** - * Additional execution summary information - * if supported by the client. - */ - ExecutionSummary ExecutionSummary `json:"executionSummary,omitempty"` -} - -/** - * A change describing how to move a `NotebookCell` - * array from state S to S'. - * - * @since 3.17.0 - proposed state - */ -type NotebookCellArrayChange = struct { - /** - * The start oftest of the cell that changed. - */ - Start uint32 `json:"start"` - /** - * The deleted cells - */ - DeleteCount uint32 `json:"deleteCount"` - /** - * The new cells, if any - */ - Cells []NotebookCell `json:"cells,omitempty"` -} - -/** - * A notebook cell kind. - * - * @since 3.17.0 - proposed state - */ -type NotebookCellKind float64 - -/** - * A notebook cell text document filter denotes a cell text - * document by different properties. - * - * @since 3.17.0 - proposed state. - */ -type NotebookCellTextDocumentFilter = struct { - /** - * A filter that matches against the notebook - * containing the notebook cell. - */ - NotebookDocument NotebookDocumentFilter `json:"notebookDocument"` - /** - * A language id like `python`. - * - * Will be matched against the language id of the - * notebook cell document. - */ - CellLanguage string `json:"cellLanguage,omitempty"` -} - -/** - * A notebook document. - * - * @since 3.17.0 - proposed state - */ -type NotebookDocument = struct { - /** - * The notebook document's uri. - */ - URI URI `json:"uri"` - /** - * The type of the notebook. - */ - NotebookType string `json:"notebookType"` - /** - * The version number of this document (it will increase after each - * change, including undo/redo). - */ - Version int32 `json:"version"` - /** - * Additional metadata stored with the notebook - * document. - */ - Metadata LSPObject `json:"metadata,omitempty"` - /** - * The cells of a notebook. - */ - Cells []NotebookCell `json:"cells"` -} - -/** - * A change event for a notebook document. - * - * @since 3.17.0 - proposed state - */ -type NotebookDocumentChangeEvent = struct { - /** - * The changed meta data if any. - */ - Metadata LSPObject `json:"metadata,omitempty"` - /** - * Changes to cells - */ - Cells struct { - /** - * Changes to the cell structure to add or - * remove cells. - */ - Structure struct { - /** - * The change to the cell array. - */ - Array NotebookCellArrayChange `json:"array"` - /** - * Additional opened cell text documents. - */ - DidOpen []TextDocumentItem `json:"didOpen,omitempty"` - /** - * Additional closed cell text documents. - */ - DidClose []TextDocumentIdentifier `json:"didClose,omitempty"` - } `json:"structure,omitempty"` - /** - * Changes to notebook cells properties like its - * kind, execution summary or metadata. - */ - Data []NotebookCell `json:"data,omitempty"` - /** - * Changes to the text content of notebook cells. - */ - TextContent []struct { - Document VersionedTextDocumentIdentifier `json:"document"` - Changes []TextDocumentContentChangeEvent `json:"changes"` - } `json:"textContent,omitempty"` - } `json:"cells,omitempty"` -} - -/** - * A notebook document filter denotes a notebook document by - * different properties. - * - * @since 3.17.0 - proposed state. - */ -type NotebookDocumentFilter = struct { - /** The type of the enclosing notebook. */ - NotebookType string `json:"notebookType"` - /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. - * Will be matched against the URI of the notebook. */ - Scheme string `json:"scheme,omitempty"` - /** A glob pattern, like `*.ipynb`. - * Will be matched against the notebooks` URI path section.*/ - Pattern string `json:"pattern,omitempty"` -} - -/** - * A literal to identify a notebook document in the client. - * - * @since 3.17.0 - proposed state - */ -type NotebookDocumentIdentifier = struct { - /** - * The notebook document's uri. - */ - URI URI `json:"uri"` -} - -/** - * A text document identifier to optionally denote a specific version of a text document. - */ -type OptionalVersionedTextDocumentIdentifier struct { - /** - * The version number of this document. If a versioned text document identifier - * is sent from the server to the client and the file is not open in the editor - * (the server has not received an open notification before) the server can send - * `null` to indicate that the version is unknown and the content on disk is the - * truth (as specified with document content ownership). - */ - Version int32/*integer | null*/ `json:"version"` - TextDocumentIdentifier -} - -/** - * Represents a parameter of a callable-signature. A parameter can - * have a label and a doc-comment. - */ -type ParameterInformation struct { - /** - * The label of this parameter information. - * - * Either a string or an inclusive start and exclusive end offsets within its containing - * signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - * string representation as `Position` and `Range` does. - * - * *Note*: a label of type string should be a substring of its containing signature label. - * Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - */ - Label string/*string | [uinteger, uinteger]*/ `json:"label"` - /** - * The human-readable doc-comment of this signature. Will be shown - * in the UI but can be omitted. - */ - Documentation string/*string | MarkupContent*/ `json:"documentation,omitempty"` -} - -type PartialResultParams struct { - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` -} - -/** - * Position in a text document expressed as zero-based line and character offset. - * The offsets are based on a UTF-16 string representation. So a string of the form - * `a𐐀b` the character offset of the character `a` is 0, the character offset of `𐐀` - * is 1 and the character offset of b is 3 since `𐐀` is represented using two code - * units in UTF-16. - * - * Positions are line end character agnostic. So you can not specify a position that - * denotes `\r|\n` or `\n|` where `|` represents the character offset. - */ -type Position struct { - /** - * Line position in a document (zero-based). - */ - Line uint32 `json:"line"` - /** - * Character offset on a line in a document (zero-based). Assuming that the line is - * represented as a string, the `character` value represents the gap between the - * `character` and `character + 1`. - * - * If the character value is greater than the line length it defaults back to the - * line length. - */ - Character uint32 `json:"character"` -} - -type PrepareRenameParams struct { - TextDocumentPositionParams - WorkDoneProgressParams -} - -type PrepareSupportDefaultBehavior = interface{} - -/** - * A previous result id in a workspace pull request. - * - * @since 3.17.0 - proposed state - */ -type PreviousResultID = struct { - /** - * The URI for which the client knowns a - * result id. - */ - URI DocumentURI `json:"uri"` - /** - * The value of the previous result id. - */ - Value string `json:"value"` -} - -type ProgressParams struct { - /** - * The progress token provided by the client or server. - */ - Token ProgressToken `json:"token"` - /** - * The progress data. - */ - Value interface{} `json:"value"` -} - -type ProgressToken = interface{} /*number | string*/ - -/** - * The publish diagnostic client capabilities. - */ -type PublishDiagnosticsClientCapabilities struct { - /** - * Whether the clients accepts diagnostics with related information. - */ - RelatedInformation bool `json:"relatedInformation,omitempty"` - /** - * Client supports the tag property to provide meta data about a diagnostic. - * Clients supporting tags have to handle unknown tags gracefully. - * - * @since 3.15.0 - */ - TagSupport struct { - /** - * The tags supported by the client. - */ - ValueSet []DiagnosticTag `json:"valueSet"` - } `json:"tagSupport,omitempty"` - /** - * Whether the client interprets the version property of the - * `textDocument/publishDiagnostics` notification`s parameter. - * - * @since 3.15.0 - */ - VersionSupport bool `json:"versionSupport,omitempty"` - /** - * Client supports a codeDescription property - * - * @since 3.16.0 - */ - CodeDescriptionSupport bool `json:"codeDescriptionSupport,omitempty"` - /** - * Whether code action supports the `data` property which is - * preserved between a `textDocument/publishDiagnostics` and - * `textDocument/codeAction` request. - * - * @since 3.16.0 - */ - DataSupport bool `json:"dataSupport,omitempty"` -} - -/** - * The publish diagnostic notification's parameters. - */ -type PublishDiagnosticsParams struct { - /** - * The URI for which diagnostic information is reported. - */ - URI DocumentURI `json:"uri"` - /** - * Optional the version number of the document the diagnostics are published for. - * - * @since 3.15.0 - */ - Version int32 `json:"version,omitempty"` - /** - * An array of diagnostic information items. - */ - Diagnostics []Diagnostic `json:"diagnostics"` -} - -/** - * A range in a text document expressed as (zero-based) start and end positions. - * - * If you want to specify a range that contains a line including the line ending - * character(s) then use an end position denoting the start of the next line. - * For example: - * ```ts - * { - * start: { line: 5, character: 23 } - * end : { line 6, character : 0 } - * } - * ``` - */ -type Range struct { - /** - * The range's start position - */ - Start Position `json:"start"` - /** - * The range's end position. - */ - End Position `json:"end"` -} - -/** - * Client Capabilities for a [ReferencesRequest](#ReferencesRequest). - */ -type ReferenceClientCapabilities struct { - /** - * Whether references supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * Value-object that contains additional information when - * requesting references. - */ -type ReferenceContext struct { - /** - * Include the declaration of the current symbol. - */ - IncludeDeclaration bool `json:"includeDeclaration"` -} - -/** - * Reference options. - */ -type ReferenceOptions struct { - WorkDoneProgressOptions -} - -/** - * Parameters for a [ReferencesRequest](#ReferencesRequest). - */ -type ReferenceParams struct { - Context ReferenceContext `json:"context"` - TextDocumentPositionParams - WorkDoneProgressParams - PartialResultParams -} - -/** - * General parameters to to register for an notification or to register a provider. - */ -type Registration struct { - /** - * The id used to register the request. The id can be used to deregister - * the request again. - */ - ID string `json:"id"` - /** - * The method to register for. - */ - Method string `json:"method"` - /** - * Options necessary for the registration. - */ - RegisterOptions LSPAny `json:"registerOptions,omitempty"` -} - -type RegistrationParams struct { - Registrations []Registration `json:"registrations"` -} - -/** - * Client capabilities specific to regular expressions. - * - * @since 3.16.0 - */ -type RegularExpressionsClientCapabilities struct { - /** - * The engine's name. - */ - Engine string `json:"engine"` - /** - * The engine's version. - */ - Version string `json:"version,omitempty"` -} - -/** - * A full diagnostic report with a set of related documents. - * - * @since 3.17.0 - proposed state - */ -type RelatedFullDocumentDiagnosticReport struct { - /** - * Diagnostics of related documents. This information is useful - * in programming languages where code in a file A can generate - * diagnostics in a file B which A depends on. An example of - * such a language is C/C++ where marco definitions in a file - * a.cpp and result in errors in a header file b.hpp. - * - * @since 3.17.0 - proposed state - */ - RelatedDocuments map[string]interface{} /*[uri: string ** DocumentUri *]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport;*/ `json:"relatedDocuments,omitempty"` -} - -/** - * An unchanged diagnostic report with a set of related documents. - * - * @since 3.17.0 - proposed state - */ -type RelatedUnchangedDocumentDiagnosticReport struct { - /** - * Diagnostics of related documents. This information is useful - * in programming languages where code in a file A can generate - * diagnostics in a file B which A depends on. An example of - * such a language is C/C++ where marco definitions in a file - * a.cpp and result in errors in a header file b.hpp. - * - * @since 3.17.0 - proposed state - */ - RelatedDocuments map[string]interface{} /*[uri: string ** DocumentUri *]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport;*/ `json:"relatedDocuments,omitempty"` -} - -type RenameClientCapabilities struct { - /** - * Whether rename supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Client supports testing for validity of rename operations - * before execution. - * - * @since 3.12.0 - */ - PrepareSupport bool `json:"prepareSupport,omitempty"` - /** - * Client supports the default behavior result. - * - * The value indicates the default behavior used by the - * client. - * - * @since 3.16.0 - */ - PrepareSupportDefaultBehavior PrepareSupportDefaultBehavior `json:"prepareSupportDefaultBehavior,omitempty"` - /** - * Whether th client honors the change annotations in - * text edits and resource operations returned via the - * rename request's workspace edit by for example presenting - * the workspace edit in the user interface and asking - * for confirmation. - * - * @since 3.16.0 - */ - HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` -} - -/** - * Rename file operation - */ -type RenameFile struct { - /** - * A rename - */ - Kind string `json:"kind"` - /** - * The old (existing) location. - */ - OldURI DocumentURI `json:"oldUri"` - /** - * The new location. - */ - NewURI DocumentURI `json:"newUri"` - /** - * Rename options. - */ - Options RenameFileOptions `json:"options,omitempty"` - ResourceOperation -} - -/** - * Rename file options - */ -type RenameFileOptions struct { - /** - * Overwrite target if existing. Overwrite wins over `ignoreIfExists` - */ - Overwrite bool `json:"overwrite,omitempty"` - /** - * Ignores if target exists. - */ - IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` -} - -/** - * The parameters sent in file rename requests/notifications. - * - * @since 3.16.0 - */ -type RenameFilesParams struct { - /** - * An array of all files/folders renamed in this operation. When a folder is renamed, only - * the folder will be included, and not its children. - */ - Files []FileRename `json:"files"` -} - -/** - * Provider options for a [RenameRequest](#RenameRequest). - */ -type RenameOptions struct { - /** - * Renames should be checked and tested before being executed. - * - * @since version 3.12.0 - */ - PrepareProvider bool `json:"prepareProvider,omitempty"` - WorkDoneProgressOptions -} - -/** - * The parameters of a [RenameRequest](#RenameRequest). - */ -type RenameParams struct { - /** - * The document to rename. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The position at which this request was sent. - */ - Position Position `json:"position"` - /** - * The new name of the symbol. If the given name is not valid the - * request must return a [ResponseError](#ResponseError) with an - * appropriate message set. - */ - NewName string `json:"newName"` - WorkDoneProgressParams -} - -/** - * A generic resource operation. - */ -type ResourceOperation struct { - /** - * The resource operation kind. - */ - Kind string `json:"kind"` - /** - * An optional annotation identifier describing the operation. - * - * @since 3.16.0 - */ - AnnotationID ChangeAnnotationIdentifier `json:"annotationId,omitempty"` -} - -type ResourceOperationKind string - -/** - * Save options. - */ -type SaveOptions struct { - /** - * The client is supposed to include the content on save. - */ - IncludeText bool `json:"includeText,omitempty"` -} - -/** - * A selection range represents a part of a selection hierarchy. A selection range - * may have a parent selection range that contains it. - */ -type SelectionRange struct { - /** - * The [range](#Range) of this selection range. - */ - Range Range `json:"range"` - /** - * The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. - */ - Parent *SelectionRange `json:"parent,omitempty"` -} - -type SelectionRangeClientCapabilities struct { - /** - * Whether implementation supports dynamic registration for selection range providers. If this is set to `true` - * the client supports the new `SelectionRangeRegistrationOptions` return value for the corresponding server - * capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -type SelectionRangeOptions struct { - WorkDoneProgressOptions -} - -/** - * A parameter literal used in selection range requests. - */ -type SelectionRangeParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The positions inside the text document. - */ - Positions []Position `json:"positions"` - WorkDoneProgressParams - PartialResultParams -} - -type SelectionRangeRegistrationOptions struct { - SelectionRangeOptions - TextDocumentRegistrationOptions - StaticRegistrationOptions -} - -/** - * @since 3.16.0 - */ -type SemanticTokens struct { - /** - * An optional result id. If provided and clients support delta updating - * the client will include the result id in the next semantic token request. - * A server can then instead of computing all semantic tokens again simply - * send a delta. - */ - ResultID string `json:"resultId,omitempty"` - /** - * The actual tokens. - */ - Data []uint32 `json:"data"` -} - -/** - * @since 3.16.0 - */ -type SemanticTokensClientCapabilities struct { - /** - * Whether implementation supports dynamic registration. If this is set to `true` - * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` - * return value for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Which requests the client supports and might send to the server - * depending on the server's capability. Please note that clients might not - * show semantic tokens or degrade some of the user experience if a range - * or full request is advertised by the client but not provided by the - * server. If for example the client capability `requests.full` and - * `request.range` are both set to true but the server only provides a - * range provider the client might not render a minimap correctly or might - * even decide to not show any semantic tokens at all. - */ - Requests struct { - /** - * The client will send the `textDocument/semanticTokens/range` request if - * the server provides a corresponding handler. - */ - Range bool/*boolean | { }*/ `json:"range,omitempty"` - /** - * The client will send the `textDocument/semanticTokens/full` request if - * the server provides a corresponding handler. - */ - Full interface{}/*boolean | */ `json:"full,omitempty"` - } `json:"requests"` - /** - * The token types that the client supports. - */ - TokenTypes []string `json:"tokenTypes"` - /** - * The token modifiers that the client supports. - */ - TokenModifiers []string `json:"tokenModifiers"` - /** - * The token formats the clients supports. - */ - Formats []TokenFormat `json:"formats"` - /** - * Whether the client supports tokens that can overlap each other. - */ - OverlappingTokenSupport bool `json:"overlappingTokenSupport,omitempty"` - /** - * Whether the client supports tokens that can span multiple lines. - */ - MultilineTokenSupport bool `json:"multilineTokenSupport,omitempty"` - /** - * Whether the client allows the server to actively cancel a - * semantic token request, e.g. supports returning - * LSPErrorCodes.ServerCancelled. If a server does the client - * needs to retrigger the request. - * - * @since 3.17.0 - */ - ServerCancelSupport bool `json:"serverCancelSupport,omitempty"` - /** - * Whether the client uses semantic tokens to augment existing - * syntax tokens. If set to `true` client side created syntax - * tokens and semantic tokens are both used for colorization. If - * set to `false` the client only uses the returned semantic tokens - * for colorization. - * - * If the value is `undefined` then the client behavior is not - * specified. - * - * @since 3.17.0 - */ - AugmentsSyntaxTokens bool `json:"augmentsSyntaxTokens,omitempty"` -} - -/** - * @since 3.16.0 - */ -type SemanticTokensDelta struct { - ResultID string `json:"resultId,omitempty"` - /** - * The semantic token edits to transform a previous result into a new result. - */ - Edits []SemanticTokensEdit `json:"edits"` -} - -/** - * @since 3.16.0 - */ -type SemanticTokensDeltaParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The result id of a previous response. The result Id can either point to a full response - * or a delta response depending on what was received last. - */ - PreviousResultID string `json:"previousResultId"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * @since 3.16.0 - */ -type SemanticTokensEdit struct { - /** - * The start offset of the edit. - */ - Start uint32 `json:"start"` - /** - * The count of elements to remove. - */ - DeleteCount uint32 `json:"deleteCount"` - /** - * The elements to insert. - */ - Data []uint32 `json:"data,omitempty"` -} - -/** - * @since 3.16.0 - */ -type SemanticTokensLegend struct { - /** - * The token types a server uses. - */ - TokenTypes []string `json:"tokenTypes"` - /** - * The token modifiers a server uses. - */ - TokenModifiers []string `json:"tokenModifiers"` -} - -/** - * @since 3.16.0 - */ -type SemanticTokensOptions struct { - /** - * The legend used by the server - */ - Legend SemanticTokensLegend `json:"legend"` - /** - * Server supports providing semantic tokens for a specific range - * of a document. - */ - Range bool/*boolean | { }*/ `json:"range,omitempty"` - /** - * Server supports providing semantic tokens for a full document. - */ - Full interface{}/*boolean | */ `json:"full,omitempty"` - WorkDoneProgressOptions -} - -/** - * @since 3.16.0 - */ -type SemanticTokensParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * @since 3.16.0 - */ -type SemanticTokensRangeParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The range the semantic tokens are requested for. - */ - Range Range `json:"range"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * @since 3.16.0 - */ -type SemanticTokensRegistrationOptions struct { - TextDocumentRegistrationOptions - SemanticTokensOptions - StaticRegistrationOptions -} - -/** - * @since 3.16.0 - */ -type SemanticTokensWorkspaceClientCapabilities struct { - /** - * Whether the client implementation supports a refresh request sent from - * the server to the client. - * - * Note that this event is global and will force the client to refresh all - * semantic tokens currently shown. It should be used with absolute care - * and is useful for situation where a server for example detects a project - * wide change that requires such a calculation. - */ - RefreshSupport bool `json:"refreshSupport,omitempty"` -} - -type ServerCapabilities struct { - /** - * Defines how text documents are synced. Is either a detailed structure defining each notification or - * for backwards compatibility the TextDocumentSyncKind number. - */ - TextDocumentSync interface{}/*TextDocumentSyncOptions | TextDocumentSyncKind*/ `json:"textDocumentSync,omitempty"` - /** - * The server provides completion support. - */ - CompletionProvider CompletionOptions `json:"completionProvider,omitempty"` - /** - * The server provides hover support. - */ - HoverProvider bool/*boolean | HoverOptions*/ `json:"hoverProvider,omitempty"` - /** - * The server provides signature help support. - */ - SignatureHelpProvider SignatureHelpOptions `json:"signatureHelpProvider,omitempty"` - /** - * The server provides Goto Declaration support. - */ - DeclarationProvider interface{}/* bool | DeclarationOptions | DeclarationRegistrationOptions*/ `json:"declarationProvider,omitempty"` - /** - * The server provides goto definition support. - */ - DefinitionProvider bool/*boolean | DefinitionOptions*/ `json:"definitionProvider,omitempty"` - /** - * The server provides Goto Type Definition support. - */ - TypeDefinitionProvider interface{}/* bool | TypeDefinitionOptions | TypeDefinitionRegistrationOptions*/ `json:"typeDefinitionProvider,omitempty"` - /** - * The server provides Goto Implementation support. - */ - ImplementationProvider interface{}/* bool | ImplementationOptions | ImplementationRegistrationOptions*/ `json:"implementationProvider,omitempty"` - /** - * The server provides find references support. - */ - ReferencesProvider bool/*boolean | ReferenceOptions*/ `json:"referencesProvider,omitempty"` - /** - * The server provides document highlight support. - */ - DocumentHighlightProvider bool/*boolean | DocumentHighlightOptions*/ `json:"documentHighlightProvider,omitempty"` - /** - * The server provides document symbol support. - */ - DocumentSymbolProvider bool/*boolean | DocumentSymbolOptions*/ `json:"documentSymbolProvider,omitempty"` - /** - * The server provides code actions. CodeActionOptions may only be - * specified if the client states that it supports - * `codeActionLiteralSupport` in its initial `initialize` request. - */ - CodeActionProvider interface{}/*boolean | CodeActionOptions*/ `json:"codeActionProvider,omitempty"` - /** - * The server provides code lens. - */ - CodeLensProvider CodeLensOptions `json:"codeLensProvider,omitempty"` - /** - * The server provides document link support. - */ - DocumentLinkProvider DocumentLinkOptions `json:"documentLinkProvider,omitempty"` - /** - * The server provides color provider support. - */ - ColorProvider interface{}/* bool | DocumentColorOptions | DocumentColorRegistrationOptions*/ `json:"colorProvider,omitempty"` - /** - * The server provides workspace symbol support. - */ - WorkspaceSymbolProvider bool/*boolean | WorkspaceSymbolOptions*/ `json:"workspaceSymbolProvider,omitempty"` - /** - * The server provides document formatting. - */ - DocumentFormattingProvider bool/*boolean | DocumentFormattingOptions*/ `json:"documentFormattingProvider,omitempty"` - /** - * The server provides document range formatting. - */ - DocumentRangeFormattingProvider bool/*boolean | DocumentRangeFormattingOptions*/ `json:"documentRangeFormattingProvider,omitempty"` - /** - * The server provides document formatting on typing. - */ - DocumentOnTypeFormattingProvider DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"` - /** - * The server provides rename support. RenameOptions may only be - * specified if the client states that it supports - * `prepareSupport` in its initial `initialize` request. - */ - RenameProvider interface{}/*boolean | RenameOptions*/ `json:"renameProvider,omitempty"` - /** - * The server provides folding provider support. - */ - FoldingRangeProvider interface{}/* bool | FoldingRangeOptions | FoldingRangeRegistrationOptions*/ `json:"foldingRangeProvider,omitempty"` - /** - * The server provides selection range support. - */ - SelectionRangeProvider interface{}/* bool | SelectionRangeOptions | SelectionRangeRegistrationOptions*/ `json:"selectionRangeProvider,omitempty"` - /** - * The server provides execute command support. - */ - ExecuteCommandProvider ExecuteCommandOptions `json:"executeCommandProvider,omitempty"` - /** - * The server provides call hierarchy support. - * - * @since 3.16.0 - */ - CallHierarchyProvider interface{}/* bool | CallHierarchyOptions | CallHierarchyRegistrationOptions*/ `json:"callHierarchyProvider,omitempty"` - /** - * The server provides linked editing range support. - * - * @since 3.16.0 - */ - LinkedEditingRangeProvider interface{}/* bool | LinkedEditingRangeOptions | LinkedEditingRangeRegistrationOptions*/ `json:"linkedEditingRangeProvider,omitempty"` - /** - * The server provides semantic tokens support. - * - * @since 3.16.0 - */ - SemanticTokensProvider interface{}/*SemanticTokensOptions | SemanticTokensRegistrationOptions*/ `json:"semanticTokensProvider,omitempty"` - /** - * The workspace server capabilities - */ - Workspace Workspace6Gn `json:"workspace,omitempty"` - /** - * The server provides moniker support. - * - * @since 3.16.0 - */ - MonikerProvider interface{}/* bool | MonikerOptions | MonikerRegistrationOptions*/ `json:"monikerProvider,omitempty"` - /** - * The server provides type hierarchy support. - * - * @since 3.17.0 - proposed state - */ - TypeHierarchyProvider interface{}/* bool | TypeHierarchyOptions | TypeHierarchyRegistrationOptions*/ `json:"typeHierarchyProvider,omitempty"` - /** - * The server provides inline values. - * - * @since 3.17.0 - proposed state - */ - InlineValueProvider interface{}/* bool | InlineValueOptions | InlineValueRegistrationOptions*/ `json:"inlineValueProvider,omitempty"` - /** - * The server provides inlay hints. - * - * @since 3.17.0 - proposed state - */ - InlayHintProvider interface{}/* bool | InlayHintOptions | InlayHintRegistrationOptions*/ `json:"inlayHintProvider,omitempty"` - /** - * Experimental server capabilities. - */ - Experimental interface{} `json:"experimental,omitempty"` -} - -type SetTraceParams struct { - Value TraceValues `json:"value"` -} - -/** - * Client capabilities for the show document request. - * - * @since 3.16.0 - */ -type ShowDocumentClientCapabilities struct { - /** - * The client has support for the show document - * request. - */ - Support bool `json:"support"` -} - -/** - * Params to show a document. - * - * @since 3.16.0 - */ -type ShowDocumentParams struct { - /** - * The document uri to show. - */ - URI URI `json:"uri"` - /** - * Indicates to show the resource in an external program. - * To show for example `https://code.visualstudio.com/` - * in the default WEB browser set `external` to `true`. - */ - External bool `json:"external,omitempty"` - /** - * An optional property to indicate whether the editor - * showing the document should take focus or not. - * Clients might ignore this property if an external - * program in started. - */ - TakeFocus bool `json:"takeFocus,omitempty"` - /** - * An optional selection range if the document is a text - * document. Clients might ignore the property if an - * external program is started or the file is not a text - * file. - */ - Selection Range `json:"selection,omitempty"` -} - -/** - * The result of an show document request. - * - * @since 3.16.0 - */ -type ShowDocumentResult struct { - /** - * A boolean indicating if the show was successful. - */ - Success bool `json:"success"` -} - -/** - * The parameters of a notification message. - */ -type ShowMessageParams struct { - /** - * The message type. See {@link MessageType} - */ - Type MessageType `json:"type"` - /** - * The actual message - */ - Message string `json:"message"` -} - -/** - * Show message request client capabilities - */ -type ShowMessageRequestClientCapabilities struct { - /** - * Capabilities specific to the `MessageActionItem` type. - */ - MessageActionItem struct { - /** - * Whether the client supports additional attributes which - * are preserved and send back to the server in the - * request's response. - */ - AdditionalPropertiesSupport bool `json:"additionalPropertiesSupport,omitempty"` - } `json:"messageActionItem,omitempty"` -} - -type ShowMessageRequestParams struct { - /** - * The message type. See {@link MessageType} - */ - Type MessageType `json:"type"` - /** - * The actual message - */ - Message string `json:"message"` - /** - * The message action items to present. - */ - Actions []MessageActionItem `json:"actions,omitempty"` -} - -/** - * Signature help represents the signature of something - * callable. There can be multiple signature but only one - * active and only one active parameter. - */ -type SignatureHelp struct { - /** - * One or more signatures. - */ - Signatures []SignatureInformation `json:"signatures"` - /** - * The active signature. If omitted or the value lies outside the - * range of `signatures` the value defaults to zero or is ignored if - * the `SignatureHelp` has no signatures. - * - * Whenever possible implementors should make an active decision about - * the active signature and shouldn't rely on a default value. - * - * In future version of the protocol this property might become - * mandatory to better express this. - */ - ActiveSignature uint32 `json:"activeSignature,omitempty"` - /** - * The active parameter of the active signature. If omitted or the value - * lies outside the range of `signatures[activeSignature].parameters` - * defaults to 0 if the active signature has parameters. If - * the active signature has no parameters it is ignored. - * In future version of the protocol this property might become - * mandatory to better express the active parameter if the - * active signature does have any. - */ - ActiveParameter uint32 `json:"activeParameter,omitempty"` -} - -/** - * Client Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). - */ -type SignatureHelpClientCapabilities struct { - /** - * Whether signature help supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports the following `SignatureInformation` - * specific properties. - */ - SignatureInformation struct { - /** - * Client supports the follow content formats for the documentation - * property. The order describes the preferred format of the client. - */ - DocumentationFormat []MarkupKind `json:"documentationFormat,omitempty"` - /** - * Client capabilities specific to parameter information. - */ - ParameterInformation struct { - /** - * The client supports processing label offsets instead of a - * simple label string. - * - * @since 3.14.0 - */ - LabelOffsetSupport bool `json:"labelOffsetSupport,omitempty"` - } `json:"parameterInformation,omitempty"` - /** - * The client support the `activeParameter` property on `SignatureInformation` - * literal. - * - * @since 3.16.0 - */ - ActiveParameterSupport bool `json:"activeParameterSupport,omitempty"` - } `json:"signatureInformation,omitempty"` - /** - * The client supports to send additional context information for a - * `textDocument/signatureHelp` request. A client that opts into - * contextSupport will also support the `retriggerCharacters` on - * `SignatureHelpOptions`. - * - * @since 3.15.0 - */ - ContextSupport bool `json:"contextSupport,omitempty"` -} - -/** - * Additional information about the context in which a signature help request was triggered. - * - * @since 3.15.0 - */ -type SignatureHelpContext struct { - /** - * Action that caused signature help to be triggered. - */ - TriggerKind SignatureHelpTriggerKind `json:"triggerKind"` - /** - * Character that caused signature help to be triggered. - * - * This is undefined when `triggerKind !== SignatureHelpTriggerKind.TriggerCharacter` - */ - TriggerCharacter string `json:"triggerCharacter,omitempty"` - /** - * `true` if signature help was already showing when it was triggered. - * - * Retrigger occurs when the signature help is already active and can be caused by actions such as - * typing a trigger character, a cursor move, or document content changes. - */ - IsRetrigger bool `json:"isRetrigger"` - /** - * The currently active `SignatureHelp`. - * - * The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on - * the user navigating through available signatures. - */ - ActiveSignatureHelp SignatureHelp `json:"activeSignatureHelp,omitempty"` -} - -/** - * Server Capabilities for a [SignatureHelpRequest](#SignatureHelpRequest). - */ -type SignatureHelpOptions struct { - /** - * List of characters that trigger signature help. - */ - TriggerCharacters []string `json:"triggerCharacters,omitempty"` - /** - * List of characters that re-trigger signature help. - * - * These trigger characters are only active when signature help is already showing. All trigger characters - * are also counted as re-trigger characters. - * - * @since 3.15.0 - */ - RetriggerCharacters []string `json:"retriggerCharacters,omitempty"` - WorkDoneProgressOptions -} - -/** - * Parameters for a [SignatureHelpRequest](#SignatureHelpRequest). - */ -type SignatureHelpParams struct { - /** - * The signature help context. This is only available if the client specifies - * to send this using the client capability `textDocument.signatureHelp.contextSupport === true` - * - * @since 3.15.0 - */ - Context SignatureHelpContext `json:"context,omitempty"` - TextDocumentPositionParams - WorkDoneProgressParams -} - -/** - * How a signature help was triggered. - * - * @since 3.15.0 - */ -type SignatureHelpTriggerKind float64 - -/** - * Represents the signature of something callable. A signature - * can have a label, like a function-name, a doc-comment, and - * a set of parameters. - */ -type SignatureInformation struct { - /** - * The label of this signature. Will be shown in - * the UI. - */ - Label string `json:"label"` - /** - * The human-readable doc-comment of this signature. Will be shown - * in the UI but can be omitted. - */ - Documentation string/*string | MarkupContent*/ `json:"documentation,omitempty"` - /** - * The parameters of this signature. - */ - Parameters []ParameterInformation `json:"parameters,omitempty"` - /** - * The index of the active parameter. - * - * If provided, this is used in place of `SignatureHelp.activeParameter`. - * - * @since 3.16.0 - */ - ActiveParameter uint32 `json:"activeParameter,omitempty"` -} - -/** - * Static registration options to be returned in the initialize - * request. - */ -type StaticRegistrationOptions struct { - /** - * The id used to register the request. The id can be used to deregister - * the request again. See also Registration#id. - */ - ID string `json:"id,omitempty"` -} - -/** - * Represents information about programming constructs like variables, classes, - * interfaces etc. - */ -type SymbolInformation struct { - /** - * The name of this symbol. - */ - Name string `json:"name"` - /** - * The kind of this symbol. - */ - Kind SymbolKind `json:"kind"` - /** - * Tags for this completion item. - * - * @since 3.16.0 - */ - Tags []SymbolTag `json:"tags,omitempty"` - /** - * Indicates if this symbol is deprecated. - * - * @deprecated Use tags instead - */ - Deprecated bool `json:"deprecated,omitempty"` - /** - * The location of this symbol. The location's range is used by a tool - * to reveal the location in the editor. If the symbol is selected in the - * tool the range's start information is used to position the cursor. So - * the range usually spans more than the actual symbol's name and does - * normally include thinks like visibility modifiers. - * - * The range doesn't have to denote a node range in the sense of a abstract - * syntax tree. It can therefore not be used to re-construct a hierarchy of - * the symbols. - */ - Location Location `json:"location"` - /** - * The name of the symbol containing this symbol. This information is for - * user interface purposes (e.g. to render a qualifier in the user interface - * if necessary). It can't be used to re-infer a hierarchy for the document - * symbols. - */ - ContainerName string `json:"containerName,omitempty"` -} - -/** - * A symbol kind. - */ -type SymbolKind float64 - -/** - * Symbol tags are extra annotations that tweak the rendering of a symbol. - * @since 3.16 - */ -type SymbolTag float64 - -/** - * Text document specific client capabilities. - */ -type TextDocumentClientCapabilities struct { - /** - * Defines which synchronization capabilities the client supports. - */ - Synchronization TextDocumentSyncClientCapabilities `json:"synchronization,omitempty"` - /** - * Capabilities specific to the `textDocument/completion` - */ - Completion CompletionClientCapabilities `json:"completion,omitempty"` - /** - * Capabilities specific to the `textDocument/hover` - */ - Hover HoverClientCapabilities `json:"hover,omitempty"` - /** - * Capabilities specific to the `textDocument/signatureHelp` - */ - SignatureHelp SignatureHelpClientCapabilities `json:"signatureHelp,omitempty"` - /** - * Capabilities specific to the `textDocument/declaration` - * - * @since 3.14.0 - */ - Declaration DeclarationClientCapabilities `json:"declaration,omitempty"` - /** - * Capabilities specific to the `textDocument/definition` - */ - Definition DefinitionClientCapabilities `json:"definition,omitempty"` - /** - * Capabilities specific to the `textDocument/typeDefinition` - * - * @since 3.6.0 - */ - TypeDefinition TypeDefinitionClientCapabilities `json:"typeDefinition,omitempty"` - /** - * Capabilities specific to the `textDocument/implementation` - * - * @since 3.6.0 - */ - Implementation ImplementationClientCapabilities `json:"implementation,omitempty"` - /** - * Capabilities specific to the `textDocument/references` - */ - References ReferenceClientCapabilities `json:"references,omitempty"` - /** - * Capabilities specific to the `textDocument/documentHighlight` - */ - DocumentHighlight DocumentHighlightClientCapabilities `json:"documentHighlight,omitempty"` - /** - * Capabilities specific to the `textDocument/documentSymbol` - */ - DocumentSymbol DocumentSymbolClientCapabilities `json:"documentSymbol,omitempty"` - /** - * Capabilities specific to the `textDocument/codeAction` - */ - CodeAction CodeActionClientCapabilities `json:"codeAction,omitempty"` - /** - * Capabilities specific to the `textDocument/codeLens` - */ - CodeLens CodeLensClientCapabilities `json:"codeLens,omitempty"` - /** - * Capabilities specific to the `textDocument/documentLink` - */ - DocumentLink DocumentLinkClientCapabilities `json:"documentLink,omitempty"` - /** - * Capabilities specific to the `textDocument/documentColor` - */ - ColorProvider DocumentColorClientCapabilities `json:"colorProvider,omitempty"` - /** - * Capabilities specific to the `textDocument/formatting` - */ - Formatting DocumentFormattingClientCapabilities `json:"formatting,omitempty"` - /** - * Capabilities specific to the `textDocument/rangeFormatting` - */ - RangeFormatting DocumentRangeFormattingClientCapabilities `json:"rangeFormatting,omitempty"` - /** - * Capabilities specific to the `textDocument/onTypeFormatting` - */ - OnTypeFormatting DocumentOnTypeFormattingClientCapabilities `json:"onTypeFormatting,omitempty"` - /** - * Capabilities specific to the `textDocument/rename` - */ - Rename RenameClientCapabilities `json:"rename,omitempty"` - /** - * Capabilities specific to `textDocument/foldingRange` request. - * - * @since 3.10.0 - */ - FoldingRange FoldingRangeClientCapabilities `json:"foldingRange,omitempty"` - /** - * Capabilities specific to `textDocument/selectionRange` request. - * - * @since 3.15.0 - */ - SelectionRange SelectionRangeClientCapabilities `json:"selectionRange,omitempty"` - /** - * Capabilities specific to `textDocument/publishDiagnostics` notification. - */ - PublishDiagnostics PublishDiagnosticsClientCapabilities `json:"publishDiagnostics,omitempty"` - /** - * Capabilities specific to the various call hierarchy request. - * - * @since 3.16.0 - */ - CallHierarchy CallHierarchyClientCapabilities `json:"callHierarchy,omitempty"` - /** - * Capabilities specific to the various semantic token request. - * - * @since 3.16.0 - */ - SemanticTokens SemanticTokensClientCapabilities `json:"semanticTokens,omitempty"` - /** - * Capabilities specific to the linked editing range request. - * - * @since 3.16.0 - */ - LinkedEditingRange LinkedEditingRangeClientCapabilities `json:"linkedEditingRange,omitempty"` - /** - * Client capabilities specific to the moniker request. - * - * @since 3.16.0 - */ - Moniker MonikerClientCapabilities `json:"moniker,omitempty"` - /** - * Capabilities specific to the various type hierarchy requests. - * - * @since 3.17.0 - proposed state - */ - TypeHierarchy TypeHierarchyClientCapabilities `json:"typeHierarchy,omitempty"` - /** - * Capabilities specific to the `textDocument/inlineValue` request. - * - * @since 3.17.0 - proposed state - */ - InlineValue InlineValueClientCapabilities `json:"inlineValue,omitempty"` - /** - * Capabilities specific to the `textDocument/inlayHint` request. - * - * @since 3.17.0 - proposed state - */ - InlayHint InlayHintClientCapabilities `json:"inlayHint,omitempty"` -} - -/** - * An event describing a change to a text document. If range and rangeLength are omitted - * the new text is considered to be the full content of the document. - */ -type TextDocumentContentChangeEvent = struct { - /** - * The range of the document that changed. - */ - Range *Range `json:"range,omitempty"` - /** - * The optional length of the range that got replaced. - * - * @deprecated use range instead. - */ - RangeLength uint32 `json:"rangeLength,omitempty"` - /** - * The new text for the provided range. - */ - Text string `json:"text"` -} - -/** - * Describes textual changes on a text document. A TextDocumentEdit describes all changes - * on a document version Si and after they are applied move the document to version Si+1. - * So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any - * kind of ordering. However the edits must be non overlapping. - */ -type TextDocumentEdit struct { - /** - * The text document to change. - */ - TextDocument OptionalVersionedTextDocumentIdentifier `json:"textDocument"` - /** - * The edits to be applied. - * - * @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a - * client capability. - */ - Edits []TextEdit/*TextEdit | AnnotatedTextEdit*/ `json:"edits"` -} - -/** - * A document filter denotes a document by different properties like - * the [language](#TextDocument.languageId), the [scheme](#Uri.scheme) of - * its resource, or a glob-pattern that is applied to the [path](#TextDocument.fileName). - * - * Glob patterns can have the following syntax: - * - `*` to match one or more characters in a path segment - * - `?` to match on one character in a path segment - * - `**` to match any number of path segments, including none - * - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) - * - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - * - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) - * - * @sample A language filter that applies to typescript files on disk: `{ language: 'typescript', scheme: 'file' }` - * @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**package.json' }` - * - * @since 3.17.0 - proposed state. - */ -type TextDocumentFilter = struct { - /** A language id, like `typescript`. */ - Language string `json:"language"` - /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. */ - Scheme string `json:"scheme,omitempty"` - /** A glob pattern, like `*.{ts,js}`. */ - Pattern string `json:"pattern,omitempty"` -} - -/** - * A literal to identify a text document in the client. - */ -type TextDocumentIdentifier struct { - /** - * The text document's uri. - */ - URI DocumentURI `json:"uri"` -} - -/** - * An item to transfer a text document from the client to the - * server. - */ -type TextDocumentItem struct { - /** - * The text document's uri. - */ - URI DocumentURI `json:"uri"` - /** - * The text document's language identifier - */ - LanguageID string `json:"languageId"` - /** - * The version number of this document (it will increase after each - * change, including undo/redo). - */ - Version int32 `json:"version"` - /** - * The content of the opened text document. - */ - Text string `json:"text"` -} - -/** - * A parameter literal used in requests to pass a text document and a position inside that - * document. - */ -type TextDocumentPositionParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The position inside the text document. - */ - Position Position `json:"position"` -} - -/** - * General text document registration options. - */ -type TextDocumentRegistrationOptions struct { - /** - * A document selector to identify the scope of the registration. If set to null - * the document selector provided on the client side will be used. - */ - DocumentSelector DocumentSelector /*DocumentSelector | null*/ `json:"documentSelector"` -} - -/** - * Represents reasons why a text document is saved. - */ -type TextDocumentSaveReason float64 - -type TextDocumentSyncClientCapabilities struct { - /** - * Whether text document synchronization supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports sending will save notifications. - */ - WillSave bool `json:"willSave,omitempty"` - /** - * The client supports sending a will save request and - * waits for a response providing text edits which will - * be applied to the document before it is saved. - */ - WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` - /** - * The client supports did save notifications. - */ - DidSave bool `json:"didSave,omitempty"` -} - -/** - * Defines how the host (editor) should sync - * document changes to the language server. - */ -type TextDocumentSyncKind float64 - -type TextDocumentSyncOptions struct { - /** - * Open and close notifications are sent to the server. If omitted open close notification should not - * be sent. - */ - OpenClose bool `json:"openClose,omitempty"` - /** - * Change notifications are sent to the server. See TextDocumentSyncKind.None, TextDocumentSyncKind.Full - * and TextDocumentSyncKind.Incremental. If omitted it defaults to TextDocumentSyncKind.None. - */ - Change TextDocumentSyncKind `json:"change,omitempty"` - /** - * If present will save notifications are sent to the server. If omitted the notification should not be - * sent. - */ - WillSave bool `json:"willSave,omitempty"` - /** - * If present will save wait until requests are sent to the server. If omitted the request should not be - * sent. - */ - WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` - /** - * If present save notifications are sent to the server. If omitted the notification should not be - * sent. - */ - Save SaveOptions/*boolean | SaveOptions*/ `json:"save,omitempty"` -} - -/** - * A text edit applicable to a text document. - */ -type TextEdit struct { - /** - * The range of the text document to be manipulated. To insert - * text into a document create a range where start === end. - */ - Range Range `json:"range"` - /** - * The string to be inserted. For delete operations use an - * empty string. - */ - NewText string `json:"newText"` -} - -type TokenFormat = string - -type TraceValues = string /* 'off' | 'messages' | 'compact' | 'verbose' */ - -/** - * Since 3.6.0 - */ -type TypeDefinitionClientCapabilities struct { - /** - * Whether implementation supports dynamic registration. If this is set to `true` - * the client supports the new `TypeDefinitionRegistrationOptions` return value - * for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * The client supports additional metadata in the form of definition links. - * - * Since 3.14.0 - */ - LinkSupport bool `json:"linkSupport,omitempty"` -} - -type TypeDefinitionOptions struct { - WorkDoneProgressOptions -} - -type TypeDefinitionParams struct { - TextDocumentPositionParams - WorkDoneProgressParams - PartialResultParams -} - -type TypeDefinitionRegistrationOptions struct { - TextDocumentRegistrationOptions - TypeDefinitionOptions - StaticRegistrationOptions -} - -/** - * @since 3.17.0 - proposed state - */ -type TypeHierarchyClientCapabilities = struct { - /** - * Whether implementation supports dynamic registration. If this is set to `true` - * the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` - * return value for the corresponding server capability as well. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - -/** - * @since 3.17.0 - proposed state - */ -type TypeHierarchyItem = struct { - /** - * The name of this item. - */ - Name string `json:"name"` - /** - * The kind of this item. - */ - Kind SymbolKind `json:"kind"` - /** - * Tags for this item. - */ - Tags []SymbolTag `json:"tags,omitempty"` - /** - * More detail for this item, e.g. the signature of a function. - */ - Detail string `json:"detail,omitempty"` - /** - * The resource identifier of this item. - */ - URI DocumentURI `json:"uri"` - /** - * The range enclosing this symbol not including leading/trailing whitespace - * but everything else, e.g. comments and code. - */ - Range *Range `json:"range"` - /** - * The range that should be selected and revealed when this symbol is being - * picked, e.g. the name of a function. Must be contained by the - * [`range`](#TypeHierarchyItem.range). - */ - SelectionRange *Range `json:"selectionRange"` - /** - * A data entry field that is preserved between a type hierarchy prepare and - * supertypes or subtypes requests. It could also be used to identify the - * type hierarchy in the server, helping improve the performance on - * resolving supertypes and subtypes. - */ - Data LSPAny `json:"data,omitempty"` -} - -/** - * Type hierarchy options used during static registration. - * - * @since 3.17.0 - proposed state - */ -type TypeHierarchyOptions = WorkDoneProgressOptions - -/** - * The parameter of a `textDocument/prepareTypeHierarchy` request. - * - * @since 3.17.0 - proposed state - */ -type TypeHierarchyPrepareParams struct { - /** - * The text document. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The position inside the text document. - */ - Position Position `json:"position"` - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` -} - -/** - * Type hierarchy options used during static or dynamic registration. - * - * @since 3.17.0 - proposed state - */ -type TypeHierarchyRegistrationOptions struct { - /** - * A document selector to identify the scope of the registration. If set to null - * the document selector provided on the client side will be used. - */ - DocumentSelector DocumentSelector/*DocumentSelector | null*/ `json:"documentSelector"` - /** - * The id used to register the request. The id can be used to deregister - * the request again. See also Registration#id. - */ - ID string `json:"id,omitempty"` -} - -/** - * The parameter of a `typeHierarchy/subtypes` request. - * - * @since 3.17.0 - proposed state - */ -type TypeHierarchySubtypesParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` - Item TypeHierarchyItem `json:"item"` -} - -/** - * The parameter of a `typeHierarchy/supertypes` request. - * - * @since 3.17.0 - proposed state - */ -type TypeHierarchySupertypesParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` - Item TypeHierarchyItem `json:"item"` -} - -/** - * A tagging type for string properties that are actually URIs - * - * @since 3.16.0 - */ -type URI = string - -/** - * A diagnostic report indicating that the last returned - * report is still accurate. - * - * @since 3.17.0 - proposed state - */ -type UnchangedDocumentDiagnosticReport = struct { - /** - * A document diagnostic report indicating - * no changes to the last result. A server can - * only return `unchanged` if result ids are - * provided. - */ - Kind string `json:"kind"` - /** - * A result id which will be sent on the next - * diagnostic request for the same document. - */ - ResultID string `json:"resultId"` -} - -/** - * Moniker uniqueness level to define scope of the moniker. - * - * @since 3.16.0 - */ -type UniquenessLevel string - -/** - * General parameters to unregister a request or notification. - */ -type Unregistration struct { - /** - * The id used to unregister the request or notification. Usually an id - * provided during the register request. - */ - ID string `json:"id"` - /** - * The method to unregister for. - */ - Method string `json:"method"` -} - -type UnregistrationParams struct { - Unregisterations []Unregistration `json:"unregisterations"` -} - -/** - * A versioned notebook document identifier. - * - * @since 3.17.0 - proposed state - */ -type VersionedNotebookDocumentIdentifier = struct { - /** - * The version number of this notebook document. - */ - Version int32 `json:"version"` - /** - * The notebook document's uri. - */ - URI URI `json:"uri"` -} - -/** - * A text document identifier to denote a specific version of a text document. - */ -type VersionedTextDocumentIdentifier struct { - /** - * The version number of this document. - */ - Version int32 `json:"version"` - TextDocumentIdentifier -} - -type WatchKind float64 - -/** - * The parameters send in a will save text document notification. - */ -type WillSaveTextDocumentParams struct { - /** - * The document that will be saved. - */ - TextDocument TextDocumentIdentifier `json:"textDocument"` - /** - * The 'TextDocumentSaveReason'. - */ - Reason TextDocumentSaveReason `json:"reason"` -} - -type WorkDoneProgressBegin struct { - Kind string `json:"kind"` - /** - * Mandatory title of the progress operation. Used to briefly inform about - * the kind of operation being performed. - * - * Examples: "Indexing" or "Linking dependencies". - */ - Title string `json:"title"` - /** - * Controls if a cancel button should show to allow the user to cancel the - * long running operation. Clients that don't support cancellation are allowed - * to ignore the setting. - */ - Cancellable bool `json:"cancellable,omitempty"` - /** - * Optional, more detailed associated progress message. Contains - * complementary information to the `title`. - * - * Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". - * If unset, the previous progress message (if any) is still valid. - */ - Message string `json:"message,omitempty"` - /** - * Optional progress percentage to display (value 100 is considered 100%). - * If not provided infinite progress is assumed and clients are allowed - * to ignore the `percentage` value in subsequent in report notifications. - * - * The value should be steadily rising. Clients are free to ignore values - * that are not following this rule. The value range is [0, 100]. - */ - Percentage uint32 `json:"percentage,omitempty"` -} - -type WorkDoneProgressCancelParams struct { - /** - * The token to be used to report progress. - */ - Token ProgressToken `json:"token"` -} - -type WorkDoneProgressClientCapabilities struct { - /** - * Window specific client capabilities. - */ - Window struct { - /** - * Whether client supports server initiated progress using the - * `window/workDoneProgress/create` request. - * - * Since 3.15.0 - */ - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` - /** - * Capabilities specific to the showMessage request. - * - * @since 3.16.0 - */ - ShowMessage ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` - /** - * Capabilities specific to the showDocument request. - * - * @since 3.16.0 - */ - ShowDocument ShowDocumentClientCapabilities `json:"showDocument,omitempty"` - } `json:"window,omitempty"` -} - -type WorkDoneProgressCreateParams struct { - /** - * The token to be used to report progress. - */ - Token ProgressToken `json:"token"` -} - -type WorkDoneProgressEnd struct { - Kind string `json:"kind"` - /** - * Optional, a final message indicating to for example indicate the outcome - * of the operation. - */ - Message string `json:"message,omitempty"` -} - -type WorkDoneProgressOptions struct { - WorkDoneProgress bool `json:"workDoneProgress,omitempty"` -} - -type WorkDoneProgressParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` -} - -type WorkDoneProgressReport struct { - Kind string `json:"kind"` - /** - * Controls enablement state of a cancel button. - * - * Clients that don't support cancellation or don't support controlling the button's - * enablement state are allowed to ignore the property. - */ - Cancellable bool `json:"cancellable,omitempty"` - /** - * Optional, more detailed associated progress message. Contains - * complementary information to the `title`. - * - * Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". - * If unset, the previous progress message (if any) is still valid. - */ - Message string `json:"message,omitempty"` - /** - * Optional progress percentage to display (value 100 is considered 100%). - * If not provided infinite progress is assumed and clients are allowed - * to ignore the `percentage` value in subsequent in report notifications. - * - * The value should be steadily rising. Clients are free to ignore values - * that are not following this rule. The value range is [0, 100] - */ - Percentage uint32 `json:"percentage,omitempty"` -} - -/** - * Workspace specific client capabilities. - */ -type WorkspaceClientCapabilities struct { - /** - * The client supports applying batch edits - * to the workspace by supporting the request - * 'workspace/applyEdit' - */ - ApplyEdit bool `json:"applyEdit,omitempty"` - /** - * Capabilities specific to `WorkspaceEdit`s - */ - WorkspaceEdit WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` - /** - * Capabilities specific to the `workspace/didChangeConfiguration` notification. - */ - DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` - /** - * Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - */ - DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` - /** - * Capabilities specific to the `workspace/symbol` request. - */ - Symbol WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` - /** - * Capabilities specific to the `workspace/executeCommand` request. - */ - ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` - /** - * Capabilities specific to the semantic token requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` - /** - * Capabilities specific to the code lens requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` - /** - * The client has support for file notifications/requests for user operations on files. - * - * Since 3.16.0 - */ - FileOperations FileOperationClientCapabilities `json:"fileOperations,omitempty"` - /** - * Capabilities specific to the inline values requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` - /** - * Capabilities specific to the inlay hints requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` -} - -/** - * Parameters of the workspace diagnostic request. - * - * @since 3.17.0 - proposed state - */ -type WorkspaceDiagnosticParams struct { - /** - * An optional token that a server can use to report work done progress. - */ - WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` - /** - * An optional token that a server can use to report partial results (e.g. streaming) to - * the client. - */ - PartialResultToken ProgressToken `json:"partialResultToken,omitempty"` - /** - * The additional identifier provided during registration. - */ - Identifier string `json:"identifier,omitempty"` - /** - * The currently known diagnostic reports with their - * previous result ids. - */ - PreviousResultIds []PreviousResultID `json:"previousResultIds"` -} - -/** - * A workspace diagnostic report. - * - * @since 3.17.0 - proposed state - */ -type WorkspaceDiagnosticReport = struct { - Items []WorkspaceDocumentDiagnosticReport `json:"items"` -} - -/** - * A workspace diagnostic document report. - * - * @since 3.17.0 - proposed state - */ -type WorkspaceDocumentDiagnosticReport = interface{} /*WorkspaceFullDocumentDiagnosticReport | WorkspaceUnchangedDocumentDiagnosticReport*/ - -/** - * A workspace edit represents changes to many resources managed in the workspace. The edit - * should either provide `changes` or `documentChanges`. If documentChanges are present - * they are preferred over `changes` if the client can handle versioned document edits. - * - * Since version 3.13.0 a workspace edit can contain resource operations as well. If resource - * operations are present clients need to execute the operations in the order in which they - * are provided. So a workspace edit for example can consist of the following two changes: - * (1) a create file a.txt and (2) a text document edit which insert text into file a.txt. - * - * An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will - * cause failure of the operation. How the client recovers from the failure is described by - * the client capability: `workspace.workspaceEdit.failureHandling` - */ -type WorkspaceEdit struct { - /** - * Holds changes to existing resources. - */ - Changes map[DocumentURI][]TextEdit/*[uri: DocumentUri]: TextEdit[]*/ `json:"changes,omitempty"` - /** - * Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes - * are either an array of `TextDocumentEdit`s to express changes to n different text documents - * where each text document edit addresses a specific version of a text document. Or it can contain - * above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. - * - * Whether a client supports versioned document edits is expressed via - * `workspace.workspaceEdit.documentChanges` client capability. - * - * If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then - * only plain `TextEdit`s using the `changes` property are supported. - */ - DocumentChanges []TextDocumentEdit/*TextDocumentEdit | CreateFile | RenameFile | DeleteFile*/ `json:"documentChanges,omitempty"` - /** - * A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and - * delete file / folder operations. - * - * Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. - * - * @since 3.16.0 - */ - ChangeAnnotations map[string]ChangeAnnotationIdentifier/*[id: ChangeAnnotationIdentifier]: ChangeAnnotation;*/ `json:"changeAnnotations,omitempty"` -} - -type WorkspaceEditClientCapabilities struct { - /** - * The client supports versioned document changes in `WorkspaceEdit`s - */ - DocumentChanges bool `json:"documentChanges,omitempty"` - /** - * The resource operations the client supports. Clients should at least - * support 'create', 'rename' and 'delete' files and folders. - * - * @since 3.13.0 - */ - ResourceOperations []ResourceOperationKind `json:"resourceOperations,omitempty"` - /** - * The failure handling strategy of a client if applying the workspace edit - * fails. - * - * @since 3.13.0 - */ - FailureHandling FailureHandlingKind `json:"failureHandling,omitempty"` - /** - * Whether the client normalizes line endings to the client specific - * setting. - * If set to `true` the client will normalize line ending characters - * in a workspace edit containing to the client specific new line - * character. - * - * @since 3.16.0 - */ - NormalizesLineEndings bool `json:"normalizesLineEndings,omitempty"` - /** - * Whether the client in general supports change annotations on text edits, - * create file, rename file and delete file changes. - * - * @since 3.16.0 - */ - ChangeAnnotationSupport struct { - /** - * Whether the client groups edits with equal labels into tree nodes, - * for instance all edits labelled with "Changes in Strings" would - * be a tree node. - */ - GroupsOnLabel bool `json:"groupsOnLabel,omitempty"` - } `json:"changeAnnotationSupport,omitempty"` -} - -type WorkspaceFolder struct { - /** - * The associated URI for this workspace folder. - */ - URI string `json:"uri"` - /** - * The name of the workspace folder. Used to refer to this - * workspace folder in the user interface. - */ - Name string `json:"name"` -} - -/** - * The workspace folder change event. - */ -type WorkspaceFoldersChangeEvent struct { - /** - * The array of added workspace folders - */ - Added []WorkspaceFolder `json:"added"` - /** - * The array of the removed workspace folders - */ - Removed []WorkspaceFolder `json:"removed"` -} - -type WorkspaceFoldersClientCapabilities struct { - /** - * The workspace client capabilities - */ - Workspace Workspace7Gn `json:"workspace,omitempty"` -} - -type WorkspaceFoldersInitializeParams struct { - /** - * The actual configured workspace folders. - */ - WorkspaceFolders []WorkspaceFolder /*WorkspaceFolder[] | null*/ `json:"workspaceFolders"` -} - -type WorkspaceFoldersServerCapabilities struct { - /** - * The workspace server capabilities - */ - Workspace Workspace9Gn `json:"workspace,omitempty"` -} - -/** - * A full document diagnostic report for a workspace diagnostic result. - * - * @since 3.17.0 - proposed state - */ -type WorkspaceFullDocumentDiagnosticReport struct { - /** - * The URI for which diagnostic information is reported. - */ - URI DocumentURI `json:"uri"` - /** - * The version number for which the diagnostics are reported. - * If the document is not marked as open `null` can be provided. - */ - Version int32/*integer | null*/ `json:"version"` -} - -/** - * A special workspace symbol that supports locations without a range - * - * @since 3.17.0 - proposed state - */ -type WorkspaceSymbol struct { - /** - * The location of the symbol. - * - * See SymbolInformation#location for more details. - */ - Location Location/*Location | { uri: DocumentUri }*/ `json:"location"` - /** - * A data entry field that is preserved on a workspace symbol between a - * workspace symbol request and a workspace symbol resolve request. - */ - Data LSPAny `json:"data,omitempty"` -} - -/** - * Client capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). - */ -type WorkspaceSymbolClientCapabilities struct { - /** - * Symbol request supports dynamic registration. - */ - DynamicRegistration bool `json:"dynamicRegistration,omitempty"` - /** - * Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. - */ - SymbolKind struct { - /** - * The symbol kind values the client supports. When this - * property exists the client also guarantees that it will - * handle values outside its set gracefully and falls back - * to a default value when unknown. - * - * If this property is not present the client only supports - * the symbol kinds from `File` to `Array` as defined in - * the initial version of the protocol. - */ - ValueSet []SymbolKind `json:"valueSet,omitempty"` - } `json:"symbolKind,omitempty"` - /** - * The client supports tags on `SymbolInformation`. - * Clients supporting tags have to handle unknown tags gracefully. - * - * @since 3.16.0 - */ - TagSupport struct { - /** - * The tags supported by the client. - */ - ValueSet []SymbolTag `json:"valueSet"` - } `json:"tagSupport,omitempty"` - /** - * The client support partial workspace symbols. The client will send the - * request `workspaceSymbol/resolve` to the server to resolve additional - * properties. - * - * @since 3.17.0 - proposedState - */ - ResolveSupport struct { - /** - * The properties that a client can resolve lazily. Usually - * `location.range` - */ - Properties []string `json:"properties"` - } `json:"resolveSupport,omitempty"` -} - -/** - * Server capabilities for a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). - */ -type WorkspaceSymbolOptions struct { - /** - * The server provides support to resolve additional - * information for a workspace symbol. - * - * @since 3.17.0 - proposed state - */ - ResolveProvider bool `json:"resolveProvider,omitempty"` - WorkDoneProgressOptions -} - -/** - * The parameters of a [WorkspaceSymbolRequest](#WorkspaceSymbolRequest). - */ -type WorkspaceSymbolParams struct { - /** - * A query string to filter symbols by. Clients may send an empty - * string here to request all symbols. - */ - Query string `json:"query"` - WorkDoneProgressParams - PartialResultParams -} - -/** - * An unchanged document diagnostic report for a workspace diagnostic result. - * - * @since 3.17.0 - proposed state - */ -type WorkspaceUnchangedDocumentDiagnosticReport struct { - /** - * The URI for which diagnostic information is reported. - */ - URI DocumentURI `json:"uri"` - /** - * The version number for which the diagnostics are reported. - * If the document is not marked as open `null` can be provided. - */ - Version int32/*integer | null*/ `json:"version"` -} - -const ( - /** - * Empty kind. - */ - - Empty CodeActionKind = "" - /** - * Base kind for quickfix actions: 'quickfix' - */ - - QuickFix CodeActionKind = "quickfix" - /** - * Base kind for refactoring actions: 'refactor' - */ - - Refactor CodeActionKind = "refactor" - /** - * Base kind for refactoring extraction actions: 'refactor.extract' - * - * Example extract actions: - * - * - Extract method - * - Extract function - * - Extract variable - * - Extract interface from class - * - ... - */ - - RefactorExtract CodeActionKind = "refactor.extract" - /** - * Base kind for refactoring inline actions: 'refactor.inline' - * - * Example inline actions: - * - * - Inline function - * - Inline variable - * - Inline constant - * - ... - */ - - RefactorInline CodeActionKind = "refactor.inline" - /** - * Base kind for refactoring rewrite actions: 'refactor.rewrite' - * - * Example rewrite actions: - * - * - Convert JavaScript function to class - * - Add or remove parameter - * - Encapsulate field - * - Make method static - * - Move method to base class - * - ... - */ - - RefactorRewrite CodeActionKind = "refactor.rewrite" - /** - * Base kind for source actions: `source` - * - * Source code actions apply to the entire file. - */ - - Source CodeActionKind = "source" - /** - * Base kind for an organize imports source action: `source.organizeImports` - */ - - SourceOrganizeImports CodeActionKind = "source.organizeImports" - /** - * Base kind for auto-fix source actions: `source.fixAll`. - * - * Fix all actions automatically fix errors that have a clear fix that do not require user input. - * They should not suppress errors or perform unsafe fixes such as generating new types or classes. - * - * @since 3.15.0 - */ - - SourceFixAll CodeActionKind = "source.fixAll" - /** - * Code actions were explicitly requested by the user or by an extension. - */ - - CodeActionInvoked CodeActionTriggerKind = 1 - /** - * Code actions were requested automatically. - * - * This typically happens when current selection in a file changes, but can - * also be triggered when file content changes. - */ - - CodeActionAutomatic CodeActionTriggerKind = 2 - TextCompletion CompletionItemKind = 1 - MethodCompletion CompletionItemKind = 2 - FunctionCompletion CompletionItemKind = 3 - ConstructorCompletion CompletionItemKind = 4 - FieldCompletion CompletionItemKind = 5 - VariableCompletion CompletionItemKind = 6 - ClassCompletion CompletionItemKind = 7 - InterfaceCompletion CompletionItemKind = 8 - ModuleCompletion CompletionItemKind = 9 - PropertyCompletion CompletionItemKind = 10 - UnitCompletion CompletionItemKind = 11 - ValueCompletion CompletionItemKind = 12 - EnumCompletion CompletionItemKind = 13 - KeywordCompletion CompletionItemKind = 14 - SnippetCompletion CompletionItemKind = 15 - ColorCompletion CompletionItemKind = 16 - FileCompletion CompletionItemKind = 17 - ReferenceCompletion CompletionItemKind = 18 - FolderCompletion CompletionItemKind = 19 - EnumMemberCompletion CompletionItemKind = 20 - ConstantCompletion CompletionItemKind = 21 - StructCompletion CompletionItemKind = 22 - EventCompletion CompletionItemKind = 23 - OperatorCompletion CompletionItemKind = 24 - TypeParameterCompletion CompletionItemKind = 25 - /** - * Render a completion as obsolete, usually using a strike-out. - */ - - ComplDeprecated CompletionItemTag = 1 - /** - * Completion was triggered by typing an identifier (24x7 code - * complete), manual invocation (e.g Ctrl+Space) or via API. - */ - - Invoked CompletionTriggerKind = 1 - /** - * Completion was triggered by a trigger character specified by - * the `triggerCharacters` properties of the `CompletionRegistrationOptions`. - */ - - TriggerCharacter CompletionTriggerKind = 2 - /** - * Completion was re-triggered as current completion list is incomplete - */ - - TriggerForIncompleteCompletions CompletionTriggerKind = 3 - /** - * Reports an error. - */ - - SeverityError DiagnosticSeverity = 1 - /** - * Reports a warning. - */ - - SeverityWarning DiagnosticSeverity = 2 - /** - * Reports an information. - */ - - SeverityInformation DiagnosticSeverity = 3 - /** - * Reports a hint. - */ - - SeverityHint DiagnosticSeverity = 4 - /** - * Unused or unnecessary code. - * - * Clients are allowed to render diagnostics with this tag faded out instead of having - * an error squiggle. - */ - - Unnecessary DiagnosticTag = 1 - /** - * Deprecated or obsolete code. - * - * Clients are allowed to rendered diagnostics with this tag strike through. - */ - - Deprecated DiagnosticTag = 2 - /** - * A textual occurrence. - */ - - Text DocumentHighlightKind = 1 - /** - * Read-access of a symbol, like reading a variable. - */ - - Read DocumentHighlightKind = 2 - /** - * Write-access of a symbol, like writing to a variable. - */ - - Write DocumentHighlightKind = 3 - /** - * Applying the workspace change is simply aborted if one of the changes provided - * fails. All operations executed before the failing operation stay executed. - */ - - Abort FailureHandlingKind = "abort" - /** - * All operations are executed transactional. That means they either all - * succeed or no changes at all are applied to the workspace. - */ - - Transactional FailureHandlingKind = "transactional" - /** - * If the workspace edit contains only textual file changes they are executed transactional. - * If resource changes (create, rename or delete file) are part of the change the failure - * handling strategy is abort. - */ - - TextOnlyTransactional FailureHandlingKind = "textOnlyTransactional" - /** - * The client tries to undo the operations already executed. But there is no - * guarantee that this is succeeding. - */ - - Undo FailureHandlingKind = "undo" - /** - * The file got created. - */ - - Created FileChangeType = 1 - /** - * The file got changed. - */ - - Changed FileChangeType = 2 - /** - * The file got deleted. - */ - - Deleted FileChangeType = 3 - /** - * The pattern matches a file only. - */ - - FileOp FileOperationPatternKind = "file" - /** - * The pattern matches a folder only. - */ - - FolderOp FileOperationPatternKind = "folder" - /** - * Folding range for a comment - */ - Comment FoldingRangeKind = "comment" - /** - * Folding range for a imports or includes - */ - Imports FoldingRangeKind = "imports" - /** - * Folding range for a region (e.g. `#region`) - */ - Region FoldingRangeKind = "region" - /** - * If the protocol version provided by the client can't be handled by the server. - * @deprecated This initialize error got replaced by client capabilities. There is - * no version handshake in version 3.0x - */ - - UnknownProtocolVersion InitializeError = 1 - /** - * An inlay hint that for a type annotation. - */ - - Type InlayHintKind = 1 - /** - * An inlay hint that is for a parameter. - */ - - Parameter InlayHintKind = 2 - /** - * The primary text to be inserted is treated as a plain string. - */ - - PlainTextTextFormat InsertTextFormat = 1 - /** - * The primary text to be inserted is treated as a snippet. - * - * A snippet can define tab stops and placeholders with `$1`, `$2` - * and `${3:foo}`. `$0` defines the final tab stop, it defaults to - * the end of the snippet. Placeholders with equal identifiers are linked, - * that is typing in one will update others too. - * - * See also: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax - */ - - SnippetTextFormat InsertTextFormat = 2 - /** - * The insertion or replace strings is taken as it is. If the - * value is multi line the lines below the cursor will be - * inserted using the indentation defined in the string value. - * The client will not apply any kind of adjustments to the - * string. - */ - - AsIs InsertTextMode = 1 - /** - * The editor adjusts leading whitespace of new lines so that - * they match the indentation up to the cursor of the line for - * which the item is accepted. - * - * Consider a line like this: <2tabs><3tabs>foo. Accepting a - * multi line completion item is indented using 2 tabs and all - * following lines inserted will be indented using 2 tabs as well. - */ - - AdjustIndentation InsertTextMode = 2 - /** - * Plain text is supported as a content format - */ - - PlainText MarkupKind = "plaintext" - /** - * Markdown is supported as a content format - */ - - Markdown MarkupKind = "markdown" - /** - * An error message. - */ - - Error MessageType = 1 - /** - * A warning message. - */ - - Warning MessageType = 2 - /** - * An information message. - */ - - Info MessageType = 3 - /** - * A log message. - */ - - Log MessageType = 4 - /** - * The moniker represent a symbol that is imported into a project - */ - Import MonikerKind = "import" - /** - * The moniker represents a symbol that is exported from a project - */ - Export MonikerKind = "export" - /** - * The moniker represents a symbol that is local to a project (e.g. a local - * variable of a function, a class not visible outside the project, ...) - */ - Local MonikerKind = "local" - /** - * A markup-cell is formatted source that is used for display. - */ - - Markup NotebookCellKind = 1 - /** - * A code-cell is source code. - */ - - Code NotebookCellKind = 2 - /** - * Supports creating new files and folders. - */ - - Create ResourceOperationKind = "create" - /** - * Supports renaming existing files and folders. - */ - - Rename ResourceOperationKind = "rename" - /** - * Supports deleting existing files and folders. - */ - - Delete ResourceOperationKind = "delete" - /** - * Signature help was invoked manually by the user or by a command. - */ - - SigInvoked SignatureHelpTriggerKind = 1 - /** - * Signature help was triggered by a trigger character. - */ - - SigTriggerCharacter SignatureHelpTriggerKind = 2 - /** - * Signature help was triggered by the cursor moving or by the document content changing. - */ - - SigContentChange SignatureHelpTriggerKind = 3 - File SymbolKind = 1 - Module SymbolKind = 2 - Namespace SymbolKind = 3 - Package SymbolKind = 4 - Class SymbolKind = 5 - Method SymbolKind = 6 - Property SymbolKind = 7 - Field SymbolKind = 8 - Constructor SymbolKind = 9 - Enum SymbolKind = 10 - Interface SymbolKind = 11 - Function SymbolKind = 12 - Variable SymbolKind = 13 - Constant SymbolKind = 14 - String SymbolKind = 15 - Number SymbolKind = 16 - Boolean SymbolKind = 17 - Array SymbolKind = 18 - Object SymbolKind = 19 - Key SymbolKind = 20 - Null SymbolKind = 21 - EnumMember SymbolKind = 22 - Struct SymbolKind = 23 - Event SymbolKind = 24 - Operator SymbolKind = 25 - TypeParameter SymbolKind = 26 - /** - * Render a symbol as obsolete, usually using a strike-out. - */ - - DeprecatedSymbol SymbolTag = 1 - /** - * Manually triggered, e.g. by the user pressing save, by starting debugging, - * or by an API call. - */ - - Manual TextDocumentSaveReason = 1 - /** - * Automatic after a delay. - */ - - AfterDelay TextDocumentSaveReason = 2 - /** - * When the editor lost focus. - */ - - FocusOut TextDocumentSaveReason = 3 - /** - * Documents should not be synced at all. - */ - - None TextDocumentSyncKind = 0 - /** - * Documents are synced by always sending the full content - * of the document. - */ - - Full TextDocumentSyncKind = 1 - /** - * Documents are synced by sending the full content on open. - * After that only incremental updates to the document are - * send. - */ - - Incremental TextDocumentSyncKind = 2 - /** - * The moniker is only unique inside a document - */ - Document UniquenessLevel = "document" - /** - * The moniker is unique inside a project for which a dump got created - */ - Project UniquenessLevel = "project" - /** - * The moniker is unique inside the group to which a project belongs - */ - Group UniquenessLevel = "group" - /** - * The moniker is unique inside the moniker scheme. - */ - Scheme UniquenessLevel = "scheme" - /** - * The moniker is globally unique - */ - Global UniquenessLevel = "global" - /** - * Interested in create events. - */ - - WatchCreate WatchKind = 1 - /** - * Interested in change events - */ - - WatchChange WatchKind = 2 - /** - * Interested in delete events - */ - - WatchDelete WatchKind = 4 -) - -// Types created to name formal parameters and embedded structs -type ParamConfiguration struct { - ConfigurationParams - PartialResultParams -} -type ParamInitialize struct { - InitializeParams - WorkDoneProgressParams -} -type PrepareRename2Gn struct { - Range Range `json:"range"` - Placeholder string `json:"placeholder"` -} -type Workspace3Gn struct { - /** - * The client supports applying batch edits - * to the workspace by supporting the request - * 'workspace/applyEdit' - */ - ApplyEdit bool `json:"applyEdit,omitempty"` - - /** - * Capabilities specific to `WorkspaceEdit`s - */ - WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeConfiguration` notification. - */ - DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - */ - DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` - - /** - * Capabilities specific to the `workspace/symbol` request. - */ - Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` - - /** - * Capabilities specific to the `workspace/executeCommand` request. - */ - ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` - - /** - * Capabilities specific to the semantic token requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` - - /** - * Capabilities specific to the code lens requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` - - /** - * The client has support for file notifications/requests for user operations on files. - * - * Since 3.16.0 - */ - FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` - - /** - * Capabilities specific to the inline values requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` - - /** - * Capabilities specific to the inlay hints requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` - - /** - * The client has support for workspace folders - * - * @since 3.6.0 - */ - WorkspaceFolders bool `json:"workspaceFolders,omitempty"` - - /** - * The client supports `workspace/configuration` requests. - * - * @since 3.6.0 - */ - Configuration bool `json:"configuration,omitempty"` -} -type Workspace4Gn struct { - /** - * The client supports applying batch edits - * to the workspace by supporting the request - * 'workspace/applyEdit' - */ - ApplyEdit bool `json:"applyEdit,omitempty"` - - /** - * Capabilities specific to `WorkspaceEdit`s - */ - WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeConfiguration` notification. - */ - DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - */ - DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` - - /** - * Capabilities specific to the `workspace/symbol` request. - */ - Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` - - /** - * Capabilities specific to the `workspace/executeCommand` request. - */ - ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` - - /** - * Capabilities specific to the semantic token requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` - - /** - * Capabilities specific to the code lens requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` - - /** - * The client has support for file notifications/requests for user operations on files. - * - * Since 3.16.0 - */ - FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` - - /** - * Capabilities specific to the inline values requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` - - /** - * Capabilities specific to the inlay hints requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` - - /** - * The client has support for workspace folders - * - * @since 3.6.0 - */ - WorkspaceFolders bool `json:"workspaceFolders,omitempty"` - - /** - * The client supports `workspace/configuration` requests. - * - * @since 3.6.0 - */ - Configuration bool `json:"configuration,omitempty"` -} -type WorkspaceFolders5Gn struct { - /** - * The Server has support for workspace folders - */ - Supported bool `json:"supported,omitempty"` - - /** - * Whether the server wants to receive workspace folder - * change notifications. - * - * If a strings is provided the string is treated as a ID - * under which the notification is registered on the client - * side. The ID can be used to unregister for these events - * using the `client/unregisterCapability` request. - */ - ChangeNotifications string/*string | boolean*/ `json:"changeNotifications,omitempty"` -} -type Workspace6Gn struct { - /** - * The server is interested in notifications/requests for operations on files. - * - * @since 3.16.0 - */ - FileOperations *FileOperationOptions `json:"fileOperations,omitempty"` - - WorkspaceFolders WorkspaceFolders5Gn `json:"workspaceFolders,omitempty"` -} -type Workspace7Gn struct { - /** - * The client supports applying batch edits - * to the workspace by supporting the request - * 'workspace/applyEdit' - */ - ApplyEdit bool `json:"applyEdit,omitempty"` - - /** - * Capabilities specific to `WorkspaceEdit`s - */ - WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeConfiguration` notification. - */ - DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` - - /** - * Capabilities specific to the `workspace/didChangeWatchedFiles` notification. - */ - DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` - - /** - * Capabilities specific to the `workspace/symbol` request. - */ - Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` - - /** - * Capabilities specific to the `workspace/executeCommand` request. - */ - ExecuteCommand ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` - - /** - * Capabilities specific to the semantic token requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - SemanticTokens SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` - - /** - * Capabilities specific to the code lens requests scoped to the - * workspace. - * - * @since 3.16.0. - */ - CodeLens CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` - - /** - * The client has support for file notifications/requests for user operations on files. - * - * Since 3.16.0 - */ - FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` - - /** - * Capabilities specific to the inline values requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlineValue InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` - - /** - * Capabilities specific to the inlay hints requests scoped to the - * workspace. - * - * @since 3.17.0. - */ - InlayHint InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` - - /** - * The client has support for workspace folders - * - * @since 3.6.0 - */ - WorkspaceFolders bool `json:"workspaceFolders,omitempty"` - - /** - * The client supports `workspace/configuration` requests. - * - * @since 3.6.0 - */ - Configuration bool `json:"configuration,omitempty"` -} -type WorkspaceFolders8Gn struct { - /** - * The Server has support for workspace folders - */ - Supported bool `json:"supported,omitempty"` - - /** - * Whether the server wants to receive workspace folder - * change notifications. - * - * If a strings is provided the string is treated as a ID - * under which the notification is registered on the client - * side. The ID can be used to unregister for these events - * using the `client/unregisterCapability` request. - */ - ChangeNotifications string/*string | boolean*/ `json:"changeNotifications,omitempty"` -} -type Workspace9Gn struct { - /** - * The server is interested in notifications/requests for operations on files. - * - * @since 3.16.0 - */ - FileOperations *FileOperationOptions `json:"fileOperations,omitempty"` - - WorkspaceFolders WorkspaceFolders8Gn `json:"workspaceFolders,omitempty"` -} diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go deleted file mode 100644 index 3cfeb772a19..00000000000 --- a/internal/lsp/regtest/runner.go +++ /dev/null @@ -1,533 +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 regtest - -import ( - "bytes" - "context" - "fmt" - "io" - "io/ioutil" - "net" - "os" - "path/filepath" - "runtime/pprof" - "strings" - "sync" - "testing" - "time" - - exec "golang.org/x/sys/execabs" - - "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/lsp/cache" - "golang.org/x/tools/internal/lsp/debug" - "golang.org/x/tools/internal/lsp/fake" - "golang.org/x/tools/internal/lsp/lsprpc" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/lsp/source" - "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/xcontext" -) - -// Mode is a bitmask that defines for which execution modes a test should run. -type Mode int - -const ( - // Singleton mode uses a separate in-process gopls instance for each test, - // and communicates over pipes to mimic the gopls sidecar execution mode, - // which communicates over stdin/stderr. - Singleton Mode = 1 << iota - // Forwarded forwards connections to a shared in-process gopls instance. - Forwarded - // SeparateProcess forwards connection to a shared separate gopls process. - SeparateProcess - // Experimental enables all of the experimental configurations that are - // being developed. - Experimental -) - -// A Runner runs tests in gopls execution environments, as specified by its -// modes. For modes that share state (for example, a shared cache or common -// remote), any tests that execute on the same Runner will share the same -// state. -type Runner struct { - DefaultModes Mode - Timeout time.Duration - GoplsPath string - PrintGoroutinesOnFailure bool - TempDir string - SkipCleanup bool - OptionsHook func(*source.Options) - - mu sync.Mutex - ts *servertest.TCPServer - socketDir string - // closers is a queue of clean-up functions to run at the end of the entire - // test suite. - closers []io.Closer -} - -type runConfig struct { - editor fake.EditorConfig - sandbox fake.SandboxConfig - modes Mode - noDefaultTimeout bool - debugAddr string - skipLogs bool - skipHooks bool - optionsHook func(*source.Options) -} - -func (r *Runner) defaultConfig() *runConfig { - return &runConfig{ - modes: r.DefaultModes, - optionsHook: r.OptionsHook, - } -} - -// A RunOption augments the behavior of the test runner. -type RunOption interface { - set(*runConfig) -} - -type optionSetter func(*runConfig) - -func (f optionSetter) set(opts *runConfig) { - f(opts) -} - -// NoDefaultTimeout removes the timeout set by the -regtest_timeout flag, for -// individual tests that are expected to run longer than is reasonable for -// ordinary regression tests. -func NoDefaultTimeout() RunOption { - return optionSetter(func(opts *runConfig) { - opts.noDefaultTimeout = true - }) -} - -// ProxyFiles configures a file proxy using the given txtar-encoded string. -func ProxyFiles(txt string) RunOption { - return optionSetter(func(opts *runConfig) { - opts.sandbox.ProxyFiles = fake.UnpackTxt(txt) - }) -} - -// Modes configures the execution modes that the test should run in. -func Modes(modes Mode) RunOption { - return optionSetter(func(opts *runConfig) { - opts.modes = modes - }) -} - -// Options configures the various server and user options. -func Options(hook func(*source.Options)) RunOption { - return optionSetter(func(opts *runConfig) { - old := opts.optionsHook - opts.optionsHook = func(o *source.Options) { - if old != nil { - old(o) - } - hook(o) - } - }) -} - -func SendPID() RunOption { - return optionSetter(func(opts *runConfig) { - opts.editor.SendPID = true - }) -} - -// EditorConfig is a RunOption option that configured the regtest editor. -type EditorConfig fake.EditorConfig - -func (c EditorConfig) set(opts *runConfig) { - opts.editor = fake.EditorConfig(c) -} - -// WorkspaceFolders configures the workdir-relative workspace folders to send -// to the LSP server. By default the editor sends a single workspace folder -// corresponding to the workdir root. To explicitly configure no workspace -// folders, use WorkspaceFolders with no arguments. -func WorkspaceFolders(relFolders ...string) RunOption { - if len(relFolders) == 0 { - // Use an empty non-nil slice to signal explicitly no folders. - relFolders = []string{} - } - return optionSetter(func(opts *runConfig) { - opts.editor.WorkspaceFolders = relFolders - }) -} - -// InGOPATH configures the workspace working directory to be GOPATH, rather -// than a separate working directory for use with modules. -func InGOPATH() RunOption { - return optionSetter(func(opts *runConfig) { - opts.sandbox.InGoPath = true - }) -} - -// DebugAddress configures a debug server bound to addr. This option is -// currently only supported when executing in Singleton mode. It is intended to -// be used for long-running stress tests. -func DebugAddress(addr string) RunOption { - return optionSetter(func(opts *runConfig) { - opts.debugAddr = addr - }) -} - -// SkipLogs skips the buffering of logs during test execution. It is intended -// for long-running stress tests. -func SkipLogs() RunOption { - return optionSetter(func(opts *runConfig) { - opts.skipLogs = true - }) -} - -// InExistingDir runs the test in a pre-existing directory. If set, no initial -// files may be passed to the runner. It is intended for long-running stress -// tests. -func InExistingDir(dir string) RunOption { - return optionSetter(func(opts *runConfig) { - opts.sandbox.Workdir = dir - }) -} - -// SkipHooks allows for disabling the test runner's client hooks that are used -// for instrumenting expectations (tracking diagnostics, logs, work done, -// etc.). It is intended for performance-sensitive stress tests or benchmarks. -func SkipHooks(skip bool) RunOption { - return optionSetter(func(opts *runConfig) { - opts.skipHooks = skip - }) -} - -// GOPROXY configures the test environment to have an explicit proxy value. -// This is intended for stress tests -- to ensure their isolation, regtests -// should instead use WithProxyFiles. -func GOPROXY(goproxy string) RunOption { - return optionSetter(func(opts *runConfig) { - opts.sandbox.GOPROXY = goproxy - }) -} - -// LimitWorkspaceScope sets the LimitWorkspaceScope configuration. -func LimitWorkspaceScope() RunOption { - return optionSetter(func(opts *runConfig) { - opts.editor.LimitWorkspaceScope = true - }) -} - -type TestFunc func(t *testing.T, env *Env) - -// Run executes the test function in the default configured gopls execution -// modes. For each a test run, a new workspace is created containing the -// un-txtared files specified by filedata. -func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) { - t.Helper() - checkBuilder(t) - - tests := []struct { - name string - mode Mode - getServer func(context.Context, *testing.T, func(*source.Options)) jsonrpc2.StreamServer - }{ - {"singleton", Singleton, singletonServer}, - {"forwarded", Forwarded, r.forwardedServer}, - {"separate_process", SeparateProcess, r.separateProcessServer}, - {"experimental", Experimental, experimentalServer}, - } - - for _, tc := range tests { - tc := tc - config := r.defaultConfig() - for _, opt := range opts { - opt.set(config) - } - if config.modes&tc.mode == 0 { - continue - } - if config.debugAddr != "" && tc.mode != Singleton { - // Debugging is useful for running stress tests, but since the daemon has - // likely already been started, it would be too late to debug. - t.Fatalf("debugging regtest servers only works in Singleton mode, "+ - "got debug addr %q and mode %v", config.debugAddr, tc.mode) - } - - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - if r.Timeout != 0 && !config.noDefaultTimeout { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, r.Timeout) - defer cancel() - } else if d, ok := testenv.Deadline(t); ok { - timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup. - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, timeout) - defer cancel() - } - - ctx = debug.WithInstance(ctx, "", "off") - if config.debugAddr != "" { - di := debug.GetInstance(ctx) - di.Serve(ctx, config.debugAddr) - di.MonitorMemory(ctx) - } - - rootDir := filepath.Join(r.TempDir, filepath.FromSlash(t.Name())) - if err := os.MkdirAll(rootDir, 0755); err != nil { - t.Fatal(err) - } - files := fake.UnpackTxt(files) - if config.editor.WindowsLineEndings { - for name, data := range files { - files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")) - } - } - config.sandbox.Files = files - config.sandbox.RootDir = rootDir - sandbox, err := fake.NewSandbox(&config.sandbox) - if err != nil { - t.Fatal(err) - } - // Deferring the closure of ws until the end of the entire test suite - // has, in testing, given the LSP server time to properly shutdown and - // release any file locks held in workspace, which is a problem on - // Windows. This may still be flaky however, and in the future we need a - // better solution to ensure that all Go processes started by gopls have - // exited before we clean up. - r.AddCloser(sandbox) - ss := tc.getServer(ctx, t, config.optionsHook) - framer := jsonrpc2.NewRawStream - ls := &loggingFramer{} - if !config.skipLogs { - framer = ls.framer(jsonrpc2.NewRawStream) - } - ts := servertest.NewPipeServer(ctx, ss, framer) - env := NewEnv(ctx, t, sandbox, ts, config.editor, !config.skipHooks) - defer func() { - if t.Failed() && r.PrintGoroutinesOnFailure { - pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) - } - if t.Failed() || *printLogs { - ls.printBuffers(t.Name(), os.Stderr) - } - // For tests that failed due to a timeout, don't fail to shutdown - // because ctx is done. - closeCtx, cancel := context.WithTimeout(xcontext.Detach(ctx), 5*time.Second) - defer cancel() - if err := env.Editor.Close(closeCtx); err != nil { - t.Errorf("closing editor: %v", err) - } - }() - // Always await the initial workspace load. - env.Await(InitialWorkspaceLoad) - test(t, env) - }) - } -} - -// longBuilders maps builders that are skipped when -short is set to a -// (possibly empty) justification. -var longBuilders = map[string]string{ - "openbsd-amd64-64": "golang.org/issues/42789", - "openbsd-386-64": "golang.org/issues/42789", - "openbsd-386-68": "golang.org/issues/42789", - "openbsd-amd64-68": "golang.org/issues/42789", - "darwin-amd64-10_12": "", - "freebsd-amd64-race": "", - "illumos-amd64": "", - "netbsd-arm-bsiegert": "", - "solaris-amd64-oraclerel": "", - "windows-arm-zx2c4": "", -} - -func checkBuilder(t *testing.T) { - t.Helper() - builder := os.Getenv("GO_BUILDER_NAME") - if reason, ok := longBuilders[builder]; ok && testing.Short() { - if reason != "" { - t.Skipf("Skipping %s with -short due to %s", builder, reason) - } else { - t.Skipf("Skipping %s with -short", builder) - } - } -} - -type loggingFramer struct { - mu sync.Mutex - buf *safeBuffer -} - -// safeBuffer is a threadsafe buffer for logs. -type safeBuffer struct { - mu sync.Mutex - buf bytes.Buffer -} - -func (b *safeBuffer) Write(p []byte) (int, error) { - b.mu.Lock() - defer b.mu.Unlock() - return b.buf.Write(p) -} - -func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer { - return func(nc net.Conn) jsonrpc2.Stream { - s.mu.Lock() - framed := false - if s.buf == nil { - s.buf = &safeBuffer{buf: bytes.Buffer{}} - framed = true - } - s.mu.Unlock() - stream := f(nc) - if framed { - return protocol.LoggingStream(stream, s.buf) - } - return stream - } -} - -func (s *loggingFramer) printBuffers(testname string, w io.Writer) { - s.mu.Lock() - defer s.mu.Unlock() - - if s.buf == nil { - return - } - fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname) - s.buf.mu.Lock() - io.Copy(w, &s.buf.buf) - s.buf.mu.Unlock() - fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) -} - -func singletonServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { - return lsprpc.NewStreamServer(cache.New(optsHook), false) -} - -func experimentalServer(_ context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { - options := func(o *source.Options) { - optsHook(o) - o.EnableAllExperiments() - // ExperimentalWorkspaceModule is not (as of writing) enabled by - // source.Options.EnableAllExperiments, but we want to test it. - o.ExperimentalWorkspaceModule = true - } - return lsprpc.NewStreamServer(cache.New(options), false) -} - -func (r *Runner) forwardedServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { - ts := r.getTestServer(optsHook) - return newForwarder("tcp", ts.Addr) -} - -// getTestServer gets the shared test server instance to connect to, or creates -// one if it doesn't exist. -func (r *Runner) getTestServer(optsHook func(*source.Options)) *servertest.TCPServer { - r.mu.Lock() - defer r.mu.Unlock() - if r.ts == nil { - ctx := context.Background() - ctx = debug.WithInstance(ctx, "", "off") - ss := lsprpc.NewStreamServer(cache.New(optsHook), false) - r.ts = servertest.NewTCPServer(ctx, ss, nil) - } - return r.ts -} - -func (r *Runner) separateProcessServer(ctx context.Context, t *testing.T, optsHook func(*source.Options)) jsonrpc2.StreamServer { - // TODO(rfindley): can we use the autostart behavior here, instead of - // pre-starting the remote? - socket := r.getRemoteSocket(t) - return newForwarder("unix", socket) -} - -func newForwarder(network, address string) *lsprpc.Forwarder { - server, err := lsprpc.NewForwarder(network+";"+address, nil) - if err != nil { - // This should never happen, as we are passing an explicit address. - panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err)) - } - return server -} - -// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running -// tests. It's a trick to allow tests to find a binary to use to start a gopls -// subprocess. -const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS" - -func (r *Runner) getRemoteSocket(t *testing.T) string { - t.Helper() - r.mu.Lock() - defer r.mu.Unlock() - const daemonFile = "gopls-test-daemon" - if r.socketDir != "" { - return filepath.Join(r.socketDir, daemonFile) - } - - if r.GoplsPath == "" { - t.Fatal("cannot run tests with a separate process unless a path to a gopls binary is configured") - } - var err error - r.socketDir, err = ioutil.TempDir(r.TempDir, "gopls-regtest-socket") - if err != nil { - t.Fatalf("creating tempdir: %v", err) - } - socket := filepath.Join(r.socketDir, daemonFile) - args := []string{"serve", "-listen", "unix;" + socket, "-listen.timeout", "10s"} - cmd := exec.Command(r.GoplsPath, args...) - cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") - var stderr bytes.Buffer - cmd.Stderr = &stderr - go func() { - if err := cmd.Run(); err != nil { - panic(fmt.Sprintf("error running external gopls: %v\nstderr:\n%s", err, stderr.String())) - } - }() - return socket -} - -// AddCloser schedules a closer to be closed at the end of the test run. This -// is useful for Windows in particular, as -func (r *Runner) AddCloser(closer io.Closer) { - r.mu.Lock() - defer r.mu.Unlock() - r.closers = append(r.closers, closer) -} - -// Close cleans up resource that have been allocated to this workspace. -func (r *Runner) Close() error { - r.mu.Lock() - defer r.mu.Unlock() - - var errmsgs []string - if r.ts != nil { - if err := r.ts.Close(); err != nil { - errmsgs = append(errmsgs, err.Error()) - } - } - if r.socketDir != "" { - if err := os.RemoveAll(r.socketDir); err != nil { - errmsgs = append(errmsgs, err.Error()) - } - } - if !r.SkipCleanup { - for _, closer := range r.closers { - if err := closer.Close(); err != nil { - errmsgs = append(errmsgs, err.Error()) - } - } - if err := os.RemoveAll(r.TempDir); err != nil { - errmsgs = append(errmsgs, err.Error()) - } - } - if len(errmsgs) > 0 { - return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t")) - } - return nil -} diff --git a/internal/lsp/reset_golden.sh b/internal/lsp/reset_golden.sh deleted file mode 100755 index 2689407ca15..00000000000 --- a/internal/lsp/reset_golden.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -find ./internal/lsp/ -name *.golden -delete -go test ./internal/lsp/source -golden -go test ./internal/lsp/ -golden -go test ./internal/lsp/cmd -golden diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go deleted file mode 100644 index 6312bcb1296..00000000000 --- a/internal/lsp/source/rename.go +++ /dev/null @@ -1,367 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package source - -import ( - "bytes" - "context" - "errors" - "fmt" - "go/ast" - "go/format" - "go/token" - "go/types" - "regexp" - "strings" - - "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/diff" - "golang.org/x/tools/internal/lsp/protocol" - "golang.org/x/tools/internal/span" - "golang.org/x/tools/refactor/satisfy" -) - -type renamer struct { - ctx context.Context - fset *token.FileSet - refs []*ReferenceInfo - objsToUpdate map[types.Object]bool - hadConflicts bool - errors string - from, to string - satisfyConstraints map[satisfy.Constraint]bool - packages map[*types.Package]Package // may include additional packages that are a dep of pkg - msets typeutil.MethodSetCache - changeMethods bool -} - -type PrepareItem struct { - Range protocol.Range - Text string -} - -// PrepareRename searches for a valid renaming at position pp. -// -// The returned usererr is intended to be displayed to the user to explain why -// the prepare fails. Probably we could eliminate the redundancy in returning -// two errors, but for now this is done defensively. -func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) { - ctx, done := event.Start(ctx, "source.PrepareRename") - defer done() - - qos, err := qualifiedObjsAtProtocolPos(ctx, snapshot, f.URI(), pp) - if err != nil { - return nil, nil, err - } - node, obj, pkg := qos[0].node, qos[0].obj, qos[0].sourcePkg - if err := checkRenamable(obj); err != nil { - return nil, err, err - } - mr, err := posToMappedRange(snapshot, pkg, node.Pos(), node.End()) - if err != nil { - return nil, nil, err - } - rng, err := mr.Range() - if err != nil { - return nil, nil, err - } - if _, isImport := node.(*ast.ImportSpec); isImport { - // We're not really renaming the import path. - rng.End = rng.Start - } - return &PrepareItem{ - Range: rng, - Text: obj.Name(), - }, nil, nil -} - -// checkRenamable verifies if an obj may be renamed. -func checkRenamable(obj types.Object) error { - if v, ok := obj.(*types.Var); ok && v.Embedded() { - return errors.New("can't rename embedded fields: rename the type directly or name the field") - } - if obj.Name() == "_" { - return errors.New("can't rename \"_\"") - } - return nil -} - -// Rename returns a map of TextEdits for each file modified when renaming a -// given identifier within a package. -func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) { - ctx, done := event.Start(ctx, "source.Rename") - defer done() - - qos, err := qualifiedObjsAtProtocolPos(ctx, s, f.URI(), pp) - if err != nil { - return nil, err - } - - obj, pkg := qos[0].obj, qos[0].pkg - - if err := checkRenamable(obj); err != nil { - return nil, err - } - if obj.Name() == newName { - return nil, fmt.Errorf("old and new names are the same: %s", newName) - } - if !isValidIdentifier(newName) { - return nil, fmt.Errorf("invalid identifier to rename: %q", newName) - } - if pkg == nil || pkg.IsIllTyped() { - return nil, fmt.Errorf("package for %s is ill typed", f.URI()) - } - refs, err := references(ctx, s, qos, true, false, true) - if err != nil { - return nil, err - } - r := renamer{ - ctx: ctx, - fset: s.FileSet(), - refs: refs, - objsToUpdate: make(map[types.Object]bool), - from: obj.Name(), - to: newName, - packages: make(map[*types.Package]Package), - } - - // A renaming initiated at an interface method indicates the - // intention to rename abstract and concrete methods as needed - // to preserve assignability. - for _, ref := range refs { - if obj, ok := ref.obj.(*types.Func); ok { - recv := obj.Type().(*types.Signature).Recv() - if recv != nil && IsInterface(recv.Type().Underlying()) { - r.changeMethods = true - break - } - } - } - for _, from := range refs { - r.packages[from.pkg.GetTypes()] = from.pkg - } - - // Check that the renaming of the identifier is ok. - for _, ref := range refs { - r.check(ref.obj) - if r.hadConflicts { // one error is enough. - break - } - } - if r.hadConflicts { - return nil, fmt.Errorf(r.errors) - } - - changes, err := r.update() - if err != nil { - return nil, err - } - result := make(map[span.URI][]protocol.TextEdit) - for uri, edits := range changes { - // These edits should really be associated with FileHandles for maximal correctness. - // For now, this is good enough. - fh, err := s.GetFile(ctx, uri) - if err != nil { - return nil, err - } - data, err := fh.Read() - if err != nil { - return nil, err - } - m := protocol.NewColumnMapper(uri, data) - // Sort the edits first. - diff.SortTextEdits(edits) - protocolEdits, err := ToProtocolEdits(m, edits) - if err != nil { - return nil, err - } - result[uri] = protocolEdits - } - return result, nil -} - -// Rename all references to the identifier. -func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) { - result := make(map[span.URI][]diff.TextEdit) - seen := make(map[span.Span]bool) - - docRegexp, err := regexp.Compile(`\b` + r.from + `\b`) - if err != nil { - return nil, err - } - for _, ref := range r.refs { - refSpan, err := ref.Span() - if err != nil { - return nil, err - } - if seen[refSpan] { - continue - } - seen[refSpan] = true - - // Renaming a types.PkgName may result in the addition or removal of an identifier, - // so we deal with this separately. - if pkgName, ok := ref.obj.(*types.PkgName); ok && ref.isDeclaration { - edit, err := r.updatePkgName(pkgName) - if err != nil { - return nil, err - } - result[refSpan.URI()] = append(result[refSpan.URI()], *edit) - continue - } - - // Replace the identifier with r.to. - edit := diff.TextEdit{ - Span: refSpan, - NewText: r.to, - } - - result[refSpan.URI()] = append(result[refSpan.URI()], edit) - - if !ref.isDeclaration || ref.ident == nil { // uses do not have doc comments to update. - continue - } - - doc := r.docComment(ref.pkg, ref.ident) - if doc == nil { - continue - } - - // Perform the rename in doc comments declared in the original package. - // go/parser strips out \r\n returns from the comment text, so go - // line-by-line through the comment text to get the correct positions. - for _, comment := range doc.List { - if isDirective(comment.Text) { - continue - } - lines := strings.Split(comment.Text, "\n") - tok := r.fset.File(comment.Pos()) - commentLine := tok.Position(comment.Pos()).Line - for i, line := range lines { - lineStart := comment.Pos() - if i > 0 { - lineStart = tok.LineStart(commentLine + i) - } - for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) { - rng := span.NewRange(r.fset, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1])) - spn, err := rng.Span() - if err != nil { - return nil, err - } - result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{ - Span: spn, - NewText: r.to, - }) - } - } - } - } - - return result, nil -} - -// docComment returns the doc for an identifier. -func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup { - _, nodes, _ := pathEnclosingInterval(r.fset, pkg, id.Pos(), id.End()) - for _, node := range nodes { - switch decl := node.(type) { - case *ast.FuncDecl: - return decl.Doc - case *ast.Field: - return decl.Doc - case *ast.GenDecl: - return decl.Doc - // For {Type,Value}Spec, if the doc on the spec is absent, - // search for the enclosing GenDecl - case *ast.TypeSpec: - if decl.Doc != nil { - return decl.Doc - } - case *ast.ValueSpec: - if decl.Doc != nil { - return decl.Doc - } - case *ast.Ident: - case *ast.AssignStmt: - // *ast.AssignStmt doesn't have an associated comment group. - // So, we try to find a comment just before the identifier. - - // Try to find a comment group only for short variable declarations (:=). - if decl.Tok != token.DEFINE { - return nil - } - - var file *ast.File - for _, f := range pkg.GetSyntax() { - if f.Pos() <= id.Pos() && id.Pos() <= f.End() { - file = f - break - } - } - if file == nil { - return nil - } - - identLine := r.fset.Position(id.Pos()).Line - for _, comment := range file.Comments { - if comment.Pos() > id.Pos() { - // Comment is after the identifier. - continue - } - - lastCommentLine := r.fset.Position(comment.End()).Line - if lastCommentLine+1 == identLine { - return comment - } - } - default: - return nil - } - } - return nil -} - -// updatePkgName returns the updates to rename a pkgName in the import spec -func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) { - // Modify ImportSpec syntax to add or remove the Name as needed. - pkg := r.packages[pkgName.Pkg()] - _, path, _ := pathEnclosingInterval(r.fset, pkg, pkgName.Pos(), pkgName.Pos()) - if len(path) < 2 { - return nil, fmt.Errorf("no path enclosing interval for %s", pkgName.Name()) - } - spec, ok := path[1].(*ast.ImportSpec) - if !ok { - return nil, fmt.Errorf("failed to update PkgName for %s", pkgName.Name()) - } - - var astIdent *ast.Ident // will be nil if ident is removed - if pkgName.Imported().Name() != r.to { - // ImportSpec.Name needed - astIdent = &ast.Ident{NamePos: spec.Path.Pos(), Name: r.to} - } - - // Make a copy of the ident that just has the name and path. - updated := &ast.ImportSpec{ - Name: astIdent, - Path: spec.Path, - EndPos: spec.EndPos, - } - - rng := span.NewRange(r.fset, spec.Pos(), spec.End()) - spn, err := rng.Span() - if err != nil { - return nil, err - } - - var buf bytes.Buffer - format.Node(&buf, r.fset, updated) - newText := buf.String() - - return &diff.TextEdit{ - Span: spn, - NewText: newText, - }, nil -} diff --git a/internal/lsp/source/symbols.go b/internal/lsp/source/symbols.go deleted file mode 100644 index 074b24eba01..00000000000 --- a/internal/lsp/source/symbols.go +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package source - -import ( - "context" - "fmt" - "go/ast" - "go/types" - - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/lsp/protocol" -) - -func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.DocumentSymbol, error) { - ctx, done := event.Start(ctx, "source.DocumentSymbols") - defer done() - - pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage) - if err != nil { - return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err) - } - - info := pkg.GetTypesInfo() - q := Qualifier(pgf.File, pkg.GetTypes(), info) - - symbolsToReceiver := make(map[types.Type]int) - var symbols []protocol.DocumentSymbol - for _, decl := range pgf.File.Decls { - switch decl := decl.(type) { - case *ast.FuncDecl: - if decl.Name.Name == "_" { - continue - } - if obj := info.ObjectOf(decl.Name); obj != nil { - fs, err := funcSymbol(snapshot, pkg, decl, obj, q) - if err != nil { - return nil, err - } - // If function is a method, prepend the type of the method. - if fs.Kind == protocol.Method { - rtype := obj.Type().(*types.Signature).Recv().Type() - fs.Name = fmt.Sprintf("(%s).%s", types.TypeString(rtype, q), fs.Name) - } - symbols = append(symbols, fs) - } - case *ast.GenDecl: - for _, spec := range decl.Specs { - switch spec := spec.(type) { - case *ast.TypeSpec: - if spec.Name.Name == "_" { - continue - } - if obj := info.ObjectOf(spec.Name); obj != nil { - ts, err := typeSymbol(snapshot, pkg, info, spec, obj, q) - if err != nil { - return nil, err - } - symbols = append(symbols, ts) - symbolsToReceiver[obj.Type()] = len(symbols) - 1 - } - case *ast.ValueSpec: - for _, name := range spec.Names { - if name.Name == "_" { - continue - } - if obj := info.ObjectOf(name); obj != nil { - vs, err := varSymbol(snapshot, pkg, decl, name, obj, q) - if err != nil { - return nil, err - } - symbols = append(symbols, vs) - } - } - } - } - } - } - return symbols, nil -} - -func funcSymbol(snapshot Snapshot, pkg Package, decl *ast.FuncDecl, obj types.Object, q types.Qualifier) (protocol.DocumentSymbol, error) { - s := protocol.DocumentSymbol{ - Name: obj.Name(), - Kind: protocol.Function, - } - var err error - s.Range, err = nodeToProtocolRange(snapshot, pkg, decl) - if err != nil { - return protocol.DocumentSymbol{}, err - } - s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, decl.Name) - if err != nil { - return protocol.DocumentSymbol{}, err - } - sig, _ := obj.Type().(*types.Signature) - if sig != nil { - if sig.Recv() != nil { - s.Kind = protocol.Method - } - s.Detail += "(" - for i := 0; i < sig.Params().Len(); i++ { - if i > 0 { - s.Detail += ", " - } - param := sig.Params().At(i) - label := types.TypeString(param.Type(), q) - if param.Name() != "" { - label = fmt.Sprintf("%s %s", param.Name(), label) - } - s.Detail += label - } - s.Detail += ")" - } - return s, nil -} - -func typeSymbol(snapshot Snapshot, pkg Package, info *types.Info, spec *ast.TypeSpec, obj types.Object, qf types.Qualifier) (protocol.DocumentSymbol, error) { - s := protocol.DocumentSymbol{ - Name: obj.Name(), - } - s.Detail, _ = FormatType(obj.Type(), qf) - s.Kind = typeToKind(obj.Type()) - - var err error - s.Range, err = nodeToProtocolRange(snapshot, pkg, spec) - if err != nil { - return protocol.DocumentSymbol{}, err - } - s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, spec.Name) - if err != nil { - return protocol.DocumentSymbol{}, err - } - t, objIsStruct := obj.Type().Underlying().(*types.Struct) - st, specIsStruct := spec.Type.(*ast.StructType) - if objIsStruct && specIsStruct { - for i := 0; i < t.NumFields(); i++ { - f := t.Field(i) - child := protocol.DocumentSymbol{ - Name: f.Name(), - Kind: protocol.Field, - } - child.Detail, _ = FormatType(f.Type(), qf) - - spanNode, selectionNode := nodesForStructField(i, st) - if span, err := nodeToProtocolRange(snapshot, pkg, spanNode); err == nil { - child.Range = span - } - if span, err := nodeToProtocolRange(snapshot, pkg, selectionNode); err == nil { - child.SelectionRange = span - } - s.Children = append(s.Children, child) - } - } - - ti, objIsInterface := obj.Type().Underlying().(*types.Interface) - ai, specIsInterface := spec.Type.(*ast.InterfaceType) - if objIsInterface && specIsInterface { - for i := 0; i < ti.NumExplicitMethods(); i++ { - method := ti.ExplicitMethod(i) - child := protocol.DocumentSymbol{ - Name: method.Name(), - Kind: protocol.Method, - } - - var spanNode, selectionNode ast.Node - Methods: - for _, f := range ai.Methods.List { - for _, id := range f.Names { - if id.Name == method.Name() { - spanNode, selectionNode = f, id - break Methods - } - } - } - child.Range, err = nodeToProtocolRange(snapshot, pkg, spanNode) - if err != nil { - return protocol.DocumentSymbol{}, err - } - child.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, selectionNode) - if err != nil { - return protocol.DocumentSymbol{}, err - } - s.Children = append(s.Children, child) - } - - for i := 0; i < ti.NumEmbeddeds(); i++ { - embedded := ti.EmbeddedType(i) - nt, isNamed := embedded.(*types.Named) - if !isNamed { - continue - } - - child := protocol.DocumentSymbol{ - Name: types.TypeString(embedded, qf), - } - child.Kind = typeToKind(embedded) - var spanNode, selectionNode ast.Node - Embeddeds: - for _, f := range ai.Methods.List { - if len(f.Names) > 0 { - continue - } - - if t := info.TypeOf(f.Type); types.Identical(nt, t) { - spanNode, selectionNode = f, f.Type - break Embeddeds - } - } - child.Range, err = nodeToProtocolRange(snapshot, pkg, spanNode) - if err != nil { - return protocol.DocumentSymbol{}, err - } - child.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, selectionNode) - if err != nil { - return protocol.DocumentSymbol{}, err - } - s.Children = append(s.Children, child) - } - } - return s, nil -} - -func nodesForStructField(i int, st *ast.StructType) (span, selection ast.Node) { - j := 0 - for _, field := range st.Fields.List { - if len(field.Names) == 0 { - if i == j { - return field, field.Type - } - j++ - continue - } - for _, name := range field.Names { - if i == j { - return field, name - } - j++ - } - } - return nil, nil -} - -func varSymbol(snapshot Snapshot, pkg Package, decl ast.Node, name *ast.Ident, obj types.Object, q types.Qualifier) (protocol.DocumentSymbol, error) { - s := protocol.DocumentSymbol{ - Name: obj.Name(), - Kind: protocol.Variable, - } - if _, ok := obj.(*types.Const); ok { - s.Kind = protocol.Constant - } - var err error - s.Range, err = nodeToProtocolRange(snapshot, pkg, decl) - if err != nil { - return protocol.DocumentSymbol{}, err - } - s.SelectionRange, err = nodeToProtocolRange(snapshot, pkg, name) - if err != nil { - return protocol.DocumentSymbol{}, err - } - s.Detail = types.TypeString(obj.Type(), q) - return s, nil -} diff --git a/internal/lsp/source/workspace_symbol_test.go b/internal/lsp/source/workspace_symbol_test.go deleted file mode 100644 index 314ef785df3..00000000000 --- a/internal/lsp/source/workspace_symbol_test.go +++ /dev/null @@ -1,46 +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 source - -import ( - "testing" -) - -func TestParseQuery(t *testing.T) { - tests := []struct { - query, s string - wantMatch bool - }{ - {"", "anything", false}, - {"any", "anything", true}, - {"any$", "anything", false}, - {"ing$", "anything", true}, - {"ing$", "anythinG", true}, - {"inG$", "anything", false}, - {"^any", "anything", true}, - {"^any", "Anything", true}, - {"^Any", "anything", false}, - {"at", "anything", true}, - // TODO: this appears to be a bug in the fuzzy matching algorithm. 'At' - // should cause a case-sensitive match. - // {"At", "anything", false}, - {"At", "Anything", true}, - {"'yth", "Anything", true}, - {"'yti", "Anything", false}, - {"'any 'thing", "Anything", true}, - {"anythn nythg", "Anything", true}, - {"ntx", "Anything", false}, - {"anythn", "anything", true}, - {"ing", "anything", true}, - {"anythn nythgx", "anything", false}, - } - - for _, test := range tests { - matcher := parseQuery(test.query, newFuzzyMatcher) - if _, score := matcher([]string{test.s}); score > 0 != test.wantMatch { - t.Errorf("parseQuery(%q) match for %q: %.2g, want match: %t", test.query, test.s, score, test.wantMatch) - } - } -} diff --git a/internal/lsp/testdata/bad/bad0.go b/internal/lsp/testdata/bad/bad0.go deleted file mode 100644 index 36a4e6b95f7..00000000000 --- a/internal/lsp/testdata/bad/bad0.go +++ /dev/null @@ -1,23 +0,0 @@ -// +build go1.11 - -package bad - -import _ "golang.org/x/tools/internal/lsp/assign/internal/secret" //@diag("\"golang.org/x/tools/internal/lsp/assign/internal/secret\"", "compiler", "could not import golang.org/x/tools/internal/lsp/assign/internal/secret (invalid use of internal package golang.org/x/tools/internal/lsp/assign/internal/secret)", "error") - -func stuff() { //@item(stuff, "stuff", "func()", "func") - x := "heeeeyyyy" - random2(x) //@diag("x", "compiler", "cannot use x (variable of type string) as int value in argument to random2", "error") - random2(1) //@complete("dom", random, random2, random3) - y := 3 //@diag("y", "compiler", "y declared but not used", "error") -} - -type bob struct { //@item(bob, "bob", "struct{...}", "struct") - x int -} - -func _() { - var q int - _ = &bob{ - f: q, //@diag("f: q", "compiler", "unknown field f in struct literal", "error") - } -} diff --git a/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go b/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go deleted file mode 100644 index 4e2b12fbcd2..00000000000 --- a/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go +++ /dev/null @@ -1,8 +0,0 @@ -package extract - -func _() { - a := /* comment in the middle of a line */ 1 //@mark(exSt18, "a") - // Comment on its own line - _ = 3 + 4 //@mark(exEn18, "4") - //@extractfunc(exSt18, exEn18) -} diff --git a/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden b/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden deleted file mode 100644 index a43822a90b0..00000000000 --- a/internal/lsp/testdata/extract/extract_function/extract_basic_comment.go.golden +++ /dev/null @@ -1,17 +0,0 @@ --- functionextraction_extract_basic_comment_4_2 -- -package extract - -func _() { - /* comment in the middle of a line */ - //@mark(exSt18, "a") - // Comment on its own line - newFunction() //@mark(exEn18, "4") - //@extractfunc(exSt18, exEn18) -} - -func newFunction() { - a := 1 - - _ = 3 + 4 -} - diff --git a/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden b/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden deleted file mode 100644 index eab22a673c1..00000000000 --- a/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden +++ /dev/null @@ -1,728 +0,0 @@ --- functionextraction_extract_basic_13_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(a *A) int { - sum := a.x + a.y - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_basic_14_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(sum int) int { - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_basic_18_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func newFunction(a A) bool { - return a.x < a.y -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_basic_22_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(a A) int { - sum := a.x + a.y - return sum -} - --- functionextraction_extract_basic_23_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(sum int) int { - return sum -} - --- functionextraction_extract_basic_9_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func newFunction(a *A) bool { - return a.x < a.y -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_method_13_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(a *A) int { - sum := a.x + a.y - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_method_14_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(sum int) int { - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_method_18_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func newFunction(a A) bool { - return a.x < a.y -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_method_22_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(a A) int { - sum := a.x + a.y - return sum -} - --- functionextraction_extract_method_23_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(sum int) int { - return sum -} - --- functionextraction_extract_method_9_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func newFunction(a *A) bool { - return a.x < a.y -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_basic_13_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a *A) newMethod() int { - sum := a.x + a.y - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_basic_14_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (*A) newMethod(sum int) int { - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_basic_18_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) newMethod() bool { - return a.x < a.y -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_basic_22_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) newMethod() int { - sum := a.x + a.y - return sum -} - --- methodextraction_extract_basic_23_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (A) newMethod(sum int) int { - return sum -} - --- methodextraction_extract_basic_9_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) newMethod() bool { - return a.x < a.y -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_method_13_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a *A) newMethod() int { - sum := a.x + a.y - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_method_14_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (*A) newMethod(sum int) int { - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_method_18_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) newMethod() bool { - return a.x < a.y -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_method_22_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) newMethod() int { - sum := a.x + a.y - return sum -} - --- methodextraction_extract_method_23_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (A) newMethod(sum int) int { - return sum -} - --- methodextraction_extract_method_9_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) newMethod() bool { - return a.x < a.y -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - diff --git a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go b/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go deleted file mode 100644 index c49e5d6a017..00000000000 --- a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go +++ /dev/null @@ -1,6 +0,0 @@ -package extract - -func _() { - var _ = 1 + 2 //@suggestedfix("1", "refactor.extract") - var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract") -} diff --git a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden b/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden deleted file mode 100644 index 00ee7b4f94d..00000000000 --- a/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden +++ /dev/null @@ -1,18 +0,0 @@ --- suggestedfix_extract_basic_lit_4_10 -- -package extract - -func _() { - x := 1 - var _ = x + 2 //@suggestedfix("1", "refactor.extract") - var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract") -} - --- suggestedfix_extract_basic_lit_5_10 -- -package extract - -func _() { - var _ = 1 + 2 //@suggestedfix("1", "refactor.extract") - x := 3 + 4 - var _ = x //@suggestedfix("3 + 4", "refactor.extract") -} - diff --git a/internal/lsp/testdata/fillstruct/a.go b/internal/lsp/testdata/fillstruct/a.go deleted file mode 100644 index 5c6df6c4a7c..00000000000 --- a/internal/lsp/testdata/fillstruct/a.go +++ /dev/null @@ -1,27 +0,0 @@ -package fillstruct - -import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite") - -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite") diff --git a/internal/lsp/testdata/fillstruct/a.go.golden b/internal/lsp/testdata/fillstruct/a.go.golden deleted file mode 100644 index 5d6dbceb279..00000000000 --- a/internal/lsp/testdata/fillstruct/a.go.golden +++ /dev/null @@ -1,126 +0,0 @@ --- suggestedfix_a_11_21 -- -package fillstruct - -import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{ - foo: 0, -} //@suggestedfix("}", "refactor.rewrite") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite") - -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite") - --- suggestedfix_a_18_22 -- -package fillstruct - -import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{ - foo: 0, - bar: "", -} //@suggestedfix("}", "refactor.rewrite") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite") - -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite") - --- suggestedfix_a_25_22 -- -package fillstruct - -import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{ - bar: "", - basic: basicStruct{}, -} //@suggestedfix("}", "refactor.rewrite") - -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite") - --- suggestedfix_a_27_16 -- -package fillstruct - -import ( - "golang.org/x/tools/internal/lsp/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite") - -var _ = data.B{ - ExportedInt: 0, -} //@suggestedfix("}", "refactor.rewrite") - diff --git a/internal/lsp/testdata/fillstruct/fill_struct_package.go b/internal/lsp/testdata/fillstruct/fill_struct_package.go deleted file mode 100644 index 71f124858b3..00000000000 --- a/internal/lsp/testdata/fillstruct/fill_struct_package.go +++ /dev/null @@ -1,12 +0,0 @@ -package fillstruct - -import ( - h2 "net/http" - - "golang.org/x/tools/internal/lsp/fillstruct/data" -) - -func unexported() { - a := data.B{} //@suggestedfix("}", "refactor.rewrite") - _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite") -} diff --git a/internal/lsp/testdata/generated/generator.go b/internal/lsp/testdata/generated/generator.go deleted file mode 100644 index f26e33c8064..00000000000 --- a/internal/lsp/testdata/generated/generator.go +++ /dev/null @@ -1,5 +0,0 @@ -package generated - -func _() { - var x int //@diag("x", "compiler", "x declared but not used", "error") -} diff --git a/internal/lsp/testdata/godef/a/a_x_test.go b/internal/lsp/testdata/godef/a/a_x_test.go deleted file mode 100644 index 4631eba2c0a..00000000000 --- a/internal/lsp/testdata/godef/a/a_x_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package a_test - -import ( - "testing" -) - -func TestA2(t *testing.T) { //@TestA2,godef(TestA2, TestA2) - Nonexistant() //@diag("Nonexistant", "compiler", "undeclared name: Nonexistant", "error") -} diff --git a/internal/lsp/testdata/godef/b/b.go.golden b/internal/lsp/testdata/godef/b/b.go.golden deleted file mode 100644 index 5f7669b77ca..00000000000 --- a/internal/lsp/testdata/godef/b/b.go.golden +++ /dev/null @@ -1,454 +0,0 @@ --- AB-hoverdef -- -```go -func (a.I).B() -``` - -\@mark\(AB, \"B\"\) - -[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#I.B) --- AField-hoverdef -- -```go -field Field int -``` - -\@mark\(AField, \"Field\"\) - -[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#S.Field) --- AField2-hoverdef -- -```go -field Field2 int -``` - -\@mark\(AField2, \"Field2\"\) - -[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#R.Field2) --- AGoodbye-hoverdef -- -```go -func (a.H).Goodbye() -``` - -\@mark\(AGoodbye, \"Goodbye\"\) - -[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#H.Goodbye) --- AHello-hoverdef -- -```go -func (a.J).Hello() -``` - -\@mark\(AHello, \"Hello\"\) - -[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#J.Hello) --- AHey-hoverdef -- -```go -func (a.R).Hey() -``` - -[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#R.Hey) --- AHi-hoverdef -- -```go -func (a.A).Hi() -``` - -[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#A.Hi) --- AImport-definition -- -godef/b/b.go:5:2-43: defined here as ```go -package a ("golang.org/x/tools/internal/lsp/godef/a") -``` - -[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls) --- AImport-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 5, - "column": 2, - "offset": 112 - }, - "end": { - "line": 5, - "column": 43, - "offset": 153 - } - }, - "description": "```go\npackage a (\"golang.org/x/tools/internal/lsp/godef/a\")\n```\n\n[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls)" -} - --- AImport-hoverdef -- -```go -package a ("golang.org/x/tools/internal/lsp/godef/a") -``` - -[`a` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls) --- AString-definition -- -godef/a/a.go:26:6-7: defined here as ```go -type A string -``` - -\@mark\(AString, \"A\"\) - -[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#A) --- AString-definition-json -- -{ - "span": { - "uri": "file://godef/a/a.go", - "start": { - "line": 26, - "column": 6, - "offset": 467 - }, - "end": { - "line": 26, - "column": 7, - "offset": 468 - } - }, - "description": "```go\ntype A string\n```\n\n\\@mark\\(AString, \\\"A\\\"\\)\n\n[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#A)" -} - --- AString-hoverdef -- -```go -type A string -``` - -\@mark\(AString, \"A\"\) - -[`a.A` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#A) --- AStuff-definition -- -godef/a/a.go:28:6-12: defined here as ```go -func a.AStuff() -``` - -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff) --- AStuff-definition-json -- -{ - "span": { - "uri": "file://godef/a/a.go", - "start": { - "line": 28, - "column": 6, - "offset": 504 - }, - "end": { - "line": 28, - "column": 12, - "offset": 510 - } - }, - "description": "```go\nfunc a.AStuff()\n```\n\n[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff)" -} - --- AStuff-hoverdef -- -```go -func a.AStuff() -``` - -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff) --- S1-definition -- -godef/b/b.go:27:6-8: defined here as ```go -type S1 struct { - F1 int //@mark(S1F1, "F1") - S2 //@godef("S2", S2),mark(S1S2, "S2") - a.A //@godef("A", AString) - aAlias //@godef("a", aAlias) -} -``` - -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1) --- S1-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 27, - "column": 6, - "offset": 587 - }, - "end": { - "line": 27, - "column": 8, - "offset": 589 - } - }, - "description": "```go\ntype S1 struct {\n\tF1 int //@mark(S1F1, \"F1\")\n\tS2 //@godef(\"S2\", S2),mark(S1S2, \"S2\")\n\ta.A //@godef(\"A\", AString)\n\taAlias //@godef(\"a\", aAlias)\n}\n```\n\n[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1)" -} - --- S1-hoverdef -- -```go -type S1 struct { - F1 int //@mark(S1F1, "F1") - S2 //@godef("S2", S2),mark(S1S2, "S2") - a.A //@godef("A", AString) - aAlias //@godef("a", aAlias) -} -``` - -[`b.S1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1) --- S1F1-definition -- -godef/b/b.go:28:2-4: defined here as ```go -field F1 int -``` - -\@mark\(S1F1, \"F1\"\) - -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1) --- S1F1-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 28, - "column": 2, - "offset": 606 - }, - "end": { - "line": 28, - "column": 4, - "offset": 608 - } - }, - "description": "```go\nfield F1 int\n```\n\n\\@mark\\(S1F1, \\\"F1\\\"\\)\n\n[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1)" -} - --- S1F1-hoverdef -- -```go -field F1 int -``` - -\@mark\(S1F1, \"F1\"\) - -[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.F1) --- S1S2-definition -- -godef/b/b.go:29:2-4: defined here as ```go -field S2 S2 -``` - -\@godef\(\"S2\", S2\),mark\(S1S2, \"S2\"\) - -[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.S2) --- S1S2-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 29, - "column": 2, - "offset": 638 - }, - "end": { - "line": 29, - "column": 4, - "offset": 640 - } - }, - "description": "```go\nfield S2 S2\n```\n\n\\@godef\\(\\\"S2\\\", S2\\),mark\\(S1S2, \\\"S2\\\"\\)\n\n[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.S2)" -} - --- S1S2-hoverdef -- -```go -field S2 S2 -``` - -\@godef\(\"S2\", S2\),mark\(S1S2, \"S2\"\) - -[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S1.S2) --- S2-definition -- -godef/b/b.go:34:6-8: defined here as ```go -type S2 struct { - F1 string //@mark(S2F1, "F1") - F2 int //@mark(S2F2, "F2") - *a.A //@godef("A", AString),godef("a",AImport) -} -``` - -[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2) --- S2-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 34, - "column": 6, - "offset": 762 - }, - "end": { - "line": 34, - "column": 8, - "offset": 764 - } - }, - "description": "```go\ntype S2 struct {\n\tF1 string //@mark(S2F1, \"F1\")\n\tF2 int //@mark(S2F2, \"F2\")\n\t*a.A //@godef(\"A\", AString),godef(\"a\",AImport)\n}\n```\n\n[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2)" -} - --- S2-hoverdef -- -```go -type S2 struct { - F1 string //@mark(S2F1, "F1") - F2 int //@mark(S2F2, "F2") - *a.A //@godef("A", AString),godef("a",AImport) -} -``` - -[`b.S2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2) --- S2F1-definition -- -godef/b/b.go:35:2-4: defined here as ```go -field F1 string -``` - -\@mark\(S2F1, \"F1\"\) - -[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F1) --- S2F1-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 35, - "column": 2, - "offset": 781 - }, - "end": { - "line": 35, - "column": 4, - "offset": 783 - } - }, - "description": "```go\nfield F1 string\n```\n\n\\@mark\\(S2F1, \\\"F1\\\"\\)\n\n[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F1)" -} - --- S2F1-hoverdef -- -```go -field F1 string -``` - -\@mark\(S2F1, \"F1\"\) - -[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F1) --- S2F2-definition -- -godef/b/b.go:36:2-4: defined here as ```go -field F2 int -``` - -\@mark\(S2F2, \"F2\"\) - -[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F2) --- S2F2-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 36, - "column": 2, - "offset": 814 - }, - "end": { - "line": 36, - "column": 4, - "offset": 816 - } - }, - "description": "```go\nfield F2 int\n```\n\n\\@mark\\(S2F2, \\\"F2\\\"\\)\n\n[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F2)" -} - --- S2F2-hoverdef -- -```go -field F2 int -``` - -\@mark\(S2F2, \"F2\"\) - -[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#S2.F2) --- aAlias-definition -- -godef/b/b.go:25:6-12: defined here as ```go -type aAlias = a.A -``` - -\@mark\(aAlias, \"aAlias\"\) --- aAlias-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 25, - "column": 6, - "offset": 542 - }, - "end": { - "line": 25, - "column": 12, - "offset": 548 - } - }, - "description": "```go\ntype aAlias = a.A\n```\n\n\\@mark\\(aAlias, \\\"aAlias\\\"\\)" -} - --- aAlias-hoverdef -- -```go -type aAlias = a.A -``` - -\@mark\(aAlias, \"aAlias\"\) --- bX-definition -- -godef/b/b.go:57:7-8: defined here as ```go -const X untyped int = 0 -``` - -\@mark\(bX, \"X\"\),godef\(\"X\", bX\) - -[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#X) --- bX-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 57, - "column": 7, - "offset": 1249 - }, - "end": { - "line": 57, - "column": 8, - "offset": 1250 - } - }, - "description": "```go\nconst X untyped int = 0\n```\n\n\\@mark\\(bX, \\\"X\\\"\\),godef\\(\\\"X\\\", bX\\)\n\n[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#X)" -} - --- bX-hoverdef -- -```go -const X untyped int = 0 -``` - -\@mark\(bX, \"X\"\),godef\(\"X\", bX\) - -[`b.X` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/b?utm_source=gopls#X) --- myFoo-definition -- -godef/b/b.go:4:2-7: defined here as ```go -package myFoo ("golang.org/x/tools/internal/lsp/foo") -``` - -[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo?utm_source=gopls) --- myFoo-definition-json -- -{ - "span": { - "uri": "file://godef/b/b.go", - "start": { - "line": 4, - "column": 2, - "offset": 21 - }, - "end": { - "line": 4, - "column": 7, - "offset": 26 - } - }, - "description": "```go\npackage myFoo (\"golang.org/x/tools/internal/lsp/foo\")\n```\n\n[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo?utm_source=gopls)" -} - --- myFoo-hoverdef -- -```go -package myFoo ("golang.org/x/tools/internal/lsp/foo") -``` - -[`myFoo` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/foo?utm_source=gopls) diff --git a/internal/lsp/testdata/godef/b/h.go.golden b/internal/lsp/testdata/godef/b/h.go.golden deleted file mode 100644 index f32f0264f8f..00000000000 --- a/internal/lsp/testdata/godef/b/h.go.golden +++ /dev/null @@ -1,12 +0,0 @@ --- AStuff-hoverdef -- -```go -func AStuff() -``` - -[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff) --- AVariable-hoverdef -- -```go -var _ A -``` - -variable of type a\.A diff --git a/internal/lsp/testdata/missingfunction/literals.go.golden b/internal/lsp/testdata/missingfunction/literals.go.golden deleted file mode 100644 index 04782b9bf50..00000000000 --- a/internal/lsp/testdata/missingfunction/literals.go.golden +++ /dev/null @@ -1,29 +0,0 @@ --- suggestedfix_literals_10_2 -- -// 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 missingfunction - -type T struct{} - -func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix") -} - -func undefinedLiterals(s string, t1 T, t2 *T) { - panic("implement me!") -} --- suggestedfix_literals_6_2 -- -package missingfunction - -type T struct{} - -func literals() { - undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix") -} - -func undefinedLiterals(s string, t1 T, t2 *T) { - panic("unimplemented") -} - diff --git a/internal/lsp/testdata/missingfunction/operation.go.golden b/internal/lsp/testdata/missingfunction/operation.go.golden deleted file mode 100644 index 5e35f300534..00000000000 --- a/internal/lsp/testdata/missingfunction/operation.go.golden +++ /dev/null @@ -1,29 +0,0 @@ --- suggestedfix_operation_10_2 -- -// 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 missingfunction - -import "time" - -func operation() { - undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix") -} - -func undefinedOperation(duration time.Duration) { - panic("implement me!") -} --- suggestedfix_operation_6_2 -- -package missingfunction - -import "time" - -func operation() { - undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix") -} - -func undefinedOperation(duration time.Duration) { - panic("unimplemented") -} - diff --git a/internal/lsp/testdata/missingfunction/unique_params.go.golden b/internal/lsp/testdata/missingfunction/unique_params.go.golden deleted file mode 100644 index 74fb91a8eb2..00000000000 --- a/internal/lsp/testdata/missingfunction/unique_params.go.golden +++ /dev/null @@ -1,30 +0,0 @@ --- suggestedfix_unique_params_10_2 -- -// 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 missingfunction - -func uniqueArguments() { - var s string - var i int - undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix") -} - -func undefinedUniqueArguments(s1 string, i int, s2 string) { - panic("implement me!") -} - --- suggestedfix_unique_params_6_2 -- -package missingfunction - -func uniqueArguments() { - var s string - var i int - undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix") -} - -func undefinedUniqueArguments(s1 string, i int, s2 string) { - panic("unimplemented") -} - diff --git a/internal/lsp/testdata/rename/bad/bad.go.golden b/internal/lsp/testdata/rename/bad/bad.go.golden deleted file mode 100644 index 7f45813926a..00000000000 --- a/internal/lsp/testdata/rename/bad/bad.go.golden +++ /dev/null @@ -1,2 +0,0 @@ --- rFunc-rename -- -renaming "sFunc" to "rFunc" not possible because "golang.org/x/tools/internal/lsp/rename/bad" has errors diff --git a/internal/lsp/testdata/signature/signature_test.go.golden b/internal/lsp/testdata/signature/signature_test.go.golden deleted file mode 100644 index 3853dffc905..00000000000 --- a/internal/lsp/testdata/signature/signature_test.go.golden +++ /dev/null @@ -1,30 +0,0 @@ --- AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias)-signature -- -AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias) - --- AliasMap(a map[*signature.Alias]signature.StringAlias) (b map[*signature.Alias]signature.StringAlias, c map[*signature.Alias]signature.StringAlias)-signature -- -AliasMap(a map[*signature.Alias]signature.StringAlias) (b map[*signature.Alias]signature.StringAlias, c map[*signature.Alias]signature.StringAlias) - --- AliasSlice(a []*sig.Alias) (b sig.Alias)-signature -- -AliasSlice(a []*sig.Alias) (b sig.Alias) - --- AliasSlice(a []*signature.Alias) (b signature.Alias)-signature -- -AliasSlice(a []*signature.Alias) (b signature.Alias) - --- GetAlias() signature.Alias-signature -- -GetAlias() signature.Alias - --- GetAliasPtr() *signature.Alias-signature -- -GetAliasPtr() *signature.Alias - --- OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias-signature -- -OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias - --- OtherAliasMap(a map[signature.Alias]signature.OtherAlias, b map[signature.Alias]signature.OtherAlias) map[signature.Alias]signature.OtherAlias-signature -- -OtherAliasMap(a map[signature.Alias]signature.OtherAlias, b map[signature.Alias]signature.OtherAlias) map[signature.Alias]signature.OtherAlias - --- SetAliasSlice(a []*signature.Alias)-signature -- -SetAliasSlice(a []*signature.Alias) - --- SetOtherAliasMap(a map[*signature.Alias]signature.OtherAlias)-signature -- -SetOtherAliasMap(a map[*signature.Alias]signature.OtherAlias) - diff --git a/internal/lsp/testdata/snippets/literal.go.golden b/internal/lsp/testdata/snippets/literal.go.golden deleted file mode 100644 index f9725f73305..00000000000 --- a/internal/lsp/testdata/snippets/literal.go.golden +++ /dev/null @@ -1,6 +0,0 @@ --- X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias-signature -- -X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias - --- X(_ map[signatures.Alias]types.CoolAlias) map[signatures.Alias]types.CoolAlias-signature -- -X(_ map[signatures.Alias]types.CoolAlias) map[signatures.Alias]types.CoolAlias - diff --git a/internal/lsp/testdata/symbols/main.go b/internal/lsp/testdata/symbols/main.go deleted file mode 100644 index 8111250f349..00000000000 --- a/internal/lsp/testdata/symbols/main.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "io" -) - -var _ = 1 - -var x = 42 //@mark(symbolsx, "x"), symbol("x", "x", "Variable", "", "main.x") - -const y = 43 //@symbol("y", "y", "Constant", "", "main.y") - -type Number int //@symbol("Number", "Number", "Number", "", "main.Number") - -type Alias = string //@symbol("Alias", "Alias", "String", "", "main.Alias") - -type NumberAlias = Number //@symbol("NumberAlias", "NumberAlias", "Number", "", "main.NumberAlias") - -type ( - Boolean bool //@symbol("Boolean", "Boolean", "Boolean", "", "main.Boolean") - BoolAlias = bool //@symbol("BoolAlias", "BoolAlias", "Boolean", "", "main.BoolAlias") -) - -type Foo struct { //@mark(symbolsFoo, "Foo"), symbol("Foo", "Foo", "Struct", "", "main.Foo") - Quux //@mark(fQuux, "Quux"), symbol("Quux", "Quux", "Field", "Foo", "main.Foo.Quux") - W io.Writer //@symbol("W" , "W", "Field", "Foo", "main.Foo.W") - Bar int //@mark(fBar, "Bar"), symbol("Bar", "Bar", "Field", "Foo", "main.Foo.Bar") - baz string //@symbol("baz", "baz", "Field", "Foo", "main.Foo.baz") -} - -type Quux struct { //@symbol("Quux", "Quux", "Struct", "", "main.Quux") - X, Y float64 //@mark(qX, "X"), symbol("X", "X", "Field", "Quux", "main.X"), symbol("Y", "Y", "Field", "Quux", "main.Y") -} - -func (f Foo) Baz() string { //@symbol("(Foo).Baz", "Baz", "Method", "", "main.Foo.Baz") - return f.baz -} - -func _() {} - -func (q *Quux) Do() {} //@mark(qDo, "Do"), symbol("(*Quux).Do", "Do", "Method", "", "main.Quux.Do") - -func main() { //@symbol("main", "main", "Function", "", "main.main") - -} - -type Stringer interface { //@symbol("Stringer", "Stringer", "Interface", "", "main.Stringer") - String() string //@symbol("String", "String", "Method", "Stringer", "main.Stringer.String") -} - -type ABer interface { //@mark(ABerInterface, "ABer"), symbol("ABer", "ABer", "Interface", "", "main.ABer") - B() //@symbol("B", "B", "Method", "ABer", "main.ABer.B") - A() string //@mark(ABerA, "A"), symbol("A", "A", "Method", "ABer", "main.ABer.A") -} - -type WithEmbeddeds interface { //@symbol("WithEmbeddeds", "WithEmbeddeds", "Interface", "", "main.WithEmbeddeds") - Do() //@symbol("Do", "Do", "Method", "WithEmbeddeds", "main.WithEmbeddeds.Do") - ABer //@symbol("ABer", "ABer", "Interface", "WithEmbeddeds", "main.WithEmbeddeds.ABer") - io.Writer //@mark(ioWriter, "io.Writer"), symbol("io.Writer", "io.Writer", "Interface", "WithEmbeddeds", "main.WithEmbeddeds.Writer") -} - -func Dunk() int { return 0 } //@symbol("Dunk", "Dunk", "Function", "", "main.Dunk") - -func dunk() {} //@symbol("dunk", "dunk", "Function", "", "main.dunk") diff --git a/internal/lsp/testdata/symbols/main.go.golden b/internal/lsp/testdata/symbols/main.go.golden deleted file mode 100644 index ebb6a8a5dd1..00000000000 --- a/internal/lsp/testdata/symbols/main.go.golden +++ /dev/null @@ -1,31 +0,0 @@ --- symbols -- -x Variable 9:5-9:6 -y Constant 11:7-11:8 -Number Number 13:6-13:12 -Alias String 15:6-15:11 -NumberAlias Number 17:6-17:17 -Boolean Boolean 20:2-20:9 -BoolAlias Boolean 21:2-21:11 -Foo Struct 24:6-24:9 - Bar Field 27:2-27:5 - Quux Field 25:2-25:6 - W Field 26:2-26:3 - baz Field 28:2-28:5 -Quux Struct 31:6-31:10 - X Field 32:2-32:3 - Y Field 32:5-32:6 -(Foo).Baz Method 35:14-35:17 -(*Quux).Do Method 41:16-41:18 -main Function 43:6-43:10 -Stringer Interface 47:6-47:14 - String Method 48:2-48:8 -ABer Interface 51:6-51:10 - A Method 53:2-53:3 - B Method 52:2-52:3 -WithEmbeddeds Interface 56:6-56:19 - ABer Interface 58:2-58:6 - Do Method 57:2-57:4 - io.Writer Interface 59:2-59:11 -Dunk Function 62:6-62:10 -dunk Function 64:6-64:10 - diff --git a/internal/lsp/testdata/typeerrors/noresultvalues.go.golden b/internal/lsp/testdata/typeerrors/noresultvalues.go.golden deleted file mode 100644 index 07c54d44553..00000000000 --- a/internal/lsp/testdata/typeerrors/noresultvalues.go.golden +++ /dev/null @@ -1,14 +0,0 @@ --- suggestedfix_noresultvalues_3_19 -- -package typeerrors - -func x() { return } //@suggestedfix("nil", "quickfix") - -func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix") - --- suggestedfix_noresultvalues_5_19 -- -package typeerrors - -func x() { return nil } //@suggestedfix("nil", "quickfix") - -func y() { return } //@suggestedfix("nil", "quickfix") - diff --git a/internal/lsp/testdata/undeclared/var.go b/internal/lsp/testdata/undeclared/var.go deleted file mode 100644 index b5f9287d48d..00000000000 --- a/internal/lsp/testdata/undeclared/var.go +++ /dev/null @@ -1,14 +0,0 @@ -package undeclared - -func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix") - if 100 < 90 { - z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix") - z = 4 - } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix") - } - r() //@diag("r", "compiler", "undeclared name: r", "error") - return z -} diff --git a/internal/lsp/testdata/undeclared/var.go.golden b/internal/lsp/testdata/undeclared/var.go.golden deleted file mode 100644 index 74adbe8ffde..00000000000 --- a/internal/lsp/testdata/undeclared/var.go.golden +++ /dev/null @@ -1,51 +0,0 @@ --- suggestedfix_var_10_6 -- -package undeclared - -func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix") - if 100 < 90 { - z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix") - z = 4 - } - i := - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix") - } - r() //@diag("r", "compiler", "undeclared name: r", "error") - return z -} - --- suggestedfix_var_4_12 -- -package undeclared - -func m() int { - y := - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix") - if 100 < 90 { - z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix") - z = 4 - } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix") - } - r() //@diag("r", "compiler", "undeclared name: r", "error") - return z -} - --- suggestedfix_var_7_18 -- -package undeclared - -func m() int { - z, _ := 1+y, 11 //@diag("y", "compiler", "undeclared name: y", "error"),suggestedfix("y", "quickfix") - n := - if 100 < 90 { - z = 1 - } else if 100 > n+2 { //@diag("n", "compiler", "undeclared name: n", "error"),suggestedfix("n", "quickfix") - z = 4 - } - for i < 200 { //@diag("i", "compiler", "undeclared name: i", "error"),suggestedfix("i", "quickfix") - } - r() //@diag("r", "compiler", "undeclared name: r", "error") - return z -} - diff --git a/internal/lsp/testdata/workspacesymbol/a/a.go b/internal/lsp/testdata/workspacesymbol/a/a.go deleted file mode 100644 index 6e5a68b16fe..00000000000 --- a/internal/lsp/testdata/workspacesymbol/a/a.go +++ /dev/null @@ -1,9 +0,0 @@ -package a - -var RandomGopherVariableA = "a" //@symbol("RandomGopherVariableA", "RandomGopherVariableA", "Variable", "", "a.RandomGopherVariableA") - -const RandomGopherConstantA = "a" //@symbol("RandomGopherConstantA", "RandomGopherConstantA", "Constant", "", "a.RandomGopherConstantA") - -const ( - randomgopherinvariable = iota //@symbol("randomgopherinvariable", "randomgopherinvariable", "Constant", "", "a.randomgopherinvariable") -) diff --git a/internal/lsp/testdata/workspacesymbol/a/a.go.golden b/internal/lsp/testdata/workspacesymbol/a/a.go.golden deleted file mode 100644 index c3f088577ba..00000000000 --- a/internal/lsp/testdata/workspacesymbol/a/a.go.golden +++ /dev/null @@ -1,5 +0,0 @@ --- symbols -- -RandomGopherVariableA Variable 3:5-3:26 -RandomGopherConstantA Constant 5:7-5:28 -randomgopherinvariable Constant 8:2-8:24 - diff --git a/internal/lsp/testdata/workspacesymbol/a/a_test.go b/internal/lsp/testdata/workspacesymbol/a/a_test.go deleted file mode 100644 index 30d5340970a..00000000000 --- a/internal/lsp/testdata/workspacesymbol/a/a_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package a - -var RandomGopherTestVariableA = "a" //@symbol("RandomGopherTestVariableA", "RandomGopherTestVariableA", "Variable", "", "a.RandomGopherTestVariableA") diff --git a/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden b/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden deleted file mode 100644 index af74619439a..00000000000 --- a/internal/lsp/testdata/workspacesymbol/a/a_test.go.golden +++ /dev/null @@ -1,3 +0,0 @@ --- symbols -- -RandomGopherTestVariableA Variable 3:5-3:30 - diff --git a/internal/lsp/testdata/workspacesymbol/a/a_x_test.go b/internal/lsp/testdata/workspacesymbol/a/a_x_test.go deleted file mode 100644 index 76eb8487d8e..00000000000 --- a/internal/lsp/testdata/workspacesymbol/a/a_x_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package a_test - -var RandomGopherXTestVariableA = "a" //@symbol("RandomGopherXTestVariableA", "RandomGopherXTestVariableA", "Variable", "", "a_test.RandomGopherXTestVariableA") diff --git a/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden b/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden deleted file mode 100644 index dfd02a5c449..00000000000 --- a/internal/lsp/testdata/workspacesymbol/a/a_x_test.go.golden +++ /dev/null @@ -1,3 +0,0 @@ --- symbols -- -RandomGopherXTestVariableA Variable 3:5-3:31 - diff --git a/internal/lsp/testdata/workspacesymbol/b/b.go b/internal/lsp/testdata/workspacesymbol/b/b.go deleted file mode 100644 index 89ce0d92e06..00000000000 --- a/internal/lsp/testdata/workspacesymbol/b/b.go +++ /dev/null @@ -1,7 +0,0 @@ -package b - -var RandomGopherVariableB = "b" //@symbol("RandomGopherVariableB", "RandomGopherVariableB", "Variable", "", "b.RandomGopherVariableB") - -type RandomGopherStructB struct { //@symbol("RandomGopherStructB", "RandomGopherStructB", "Struct", "", "b.RandomGopherStructB") - Bar int //@mark(bBar, "Bar"), symbol("Bar", "Bar", "Field", "RandomGopherStructB", "b.RandomGopherStructB.Bar") -} diff --git a/internal/lsp/testdata/workspacesymbol/b/b.go.golden b/internal/lsp/testdata/workspacesymbol/b/b.go.golden deleted file mode 100644 index 4711c9d91ad..00000000000 --- a/internal/lsp/testdata/workspacesymbol/b/b.go.golden +++ /dev/null @@ -1,5 +0,0 @@ --- symbols -- -RandomGopherVariableB Variable 3:5-3:26 -RandomGopherStructB Struct 5:6-5:25 - Bar Field 6:2-6:5 - diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go index 89f79c68b7d..e56af3bb45b 100644 --- a/internal/memoize/memoize.go +++ b/internal/memoize/memoize.go @@ -2,150 +2,88 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package memoize supports memoizing the return values of functions with -// idempotent results that are expensive to compute. +// Package memoize defines a "promise" abstraction that enables +// memoization of the result of calling an expensive but idempotent +// function. // -// To use this package, build a store and use it to acquire handles with the -// Bind method. +// Call p = NewPromise(f) to obtain a promise for the future result of +// calling f(), and call p.Get() to obtain that result. All calls to +// p.Get return the result of a single call of f(). +// Get blocks if the function has not finished (or started). +// +// A Store is a map of arbitrary keys to promises. Use Store.Promise +// to create a promise in the store. All calls to Handle(k) return the +// same promise as long as it is in the store. These promises are +// reference-counted and must be explicitly released. Once the last +// reference is released, the promise is removed from the store. package memoize import ( "context" - "flag" "fmt" "reflect" + "runtime/trace" "sync" "sync/atomic" "golang.org/x/tools/internal/xcontext" ) -var ( - panicOnDestroyed = flag.Bool("memoize_panic_on_destroyed", false, - "Panic when a destroyed generation is read rather than returning an error. "+ - "Panicking may make it easier to debug lifetime errors, especially when "+ - "used with GOTRACEBACK=crash to see all running goroutines.") -) - -// Store binds keys to functions, returning handles that can be used to access -// the functions results. -type Store struct { - mu sync.Mutex - // handles is the set of values stored. - handles map[interface{}]*Handle - - // generations is the set of generations live in this store. - generations map[*Generation]struct{} -} - -// Generation creates a new Generation associated with s. Destroy must be -// called on the returned Generation once it is no longer in use. name is -// for debugging purposes only. -func (s *Store) Generation(name string) *Generation { - s.mu.Lock() - defer s.mu.Unlock() - if s.handles == nil { - s.handles = map[interface{}]*Handle{} - s.generations = map[*Generation]struct{}{} - } - g := &Generation{store: s, name: name} - s.generations[g] = struct{}{} - return g -} - -// A Generation is a logical point in time of the cache life-cycle. Cache -// entries associated with a Generation will not be removed until the -// Generation is destroyed. -type Generation struct { - // destroyed is 1 after the generation is destroyed. Atomic. - destroyed uint32 - store *Store - name string - // destroyedBy describes the caller that togged destroyed from 0 to 1. - destroyedBy string - // wg tracks the reference count of this generation. - wg sync.WaitGroup -} - -// Destroy waits for all operations referencing g to complete, then removes -// all references to g from cache entries. Cache entries that no longer -// reference any non-destroyed generation are removed. Destroy must be called -// exactly once for each generation, and destroyedBy describes the caller. -func (g *Generation) Destroy(destroyedBy string) { - g.wg.Wait() - - prevDestroyedBy := g.destroyedBy - g.destroyedBy = destroyedBy - if ok := atomic.CompareAndSwapUint32(&g.destroyed, 0, 1); !ok { - panic("Destroy on generation " + g.name + " already destroyed by " + prevDestroyedBy) - } - - g.store.mu.Lock() - defer g.store.mu.Unlock() - for k, e := range g.store.handles { - e.mu.Lock() - if _, ok := e.generations[g]; ok { - delete(e.generations, g) // delete even if it's dead, in case of dangling references to the entry. - if len(e.generations) == 0 { - delete(g.store.handles, k) - e.state = stateDestroyed - if e.cleanup != nil && e.value != nil { - e.cleanup(e.value) - } - } - } - e.mu.Unlock() - } - delete(g.store.generations, g) -} +// Function is the type of a function that can be memoized. +// +// If the arg is a RefCounted, its Acquire/Release operations are called. +// +// The argument must not materially affect the result of the function +// in ways that are not captured by the promise's key, since if +// Promise.Get is called twice concurrently, with the same (implicit) +// key but different arguments, the Function is called only once but +// its result must be suitable for both callers. +// +// The main purpose of the argument is to avoid the Function closure +// needing to retain large objects (in practice: the snapshot) in +// memory that can be supplied at call time by any caller. +type Function func(ctx context.Context, arg interface{}) interface{} -// Acquire creates a new reference to g, and returns a func to release that -// reference. -func (g *Generation) Acquire() func() { - destroyed := atomic.LoadUint32(&g.destroyed) - if destroyed != 0 { - panic("acquire on generation " + g.name + " destroyed by " + g.destroyedBy) - } - g.wg.Add(1) - return g.wg.Done +// A RefCounted is a value whose functional lifetime is determined by +// reference counting. +// +// Its Acquire method is called before the Function is invoked, and +// the corresponding release is called when the Function returns. +// Usually both events happen within a single call to Get, so Get +// would be fine with a "borrowed" reference, but if the context is +// cancelled, Get may return before the Function is complete, causing +// the argument to escape, and potential premature destruction of the +// value. For a reference-counted type, this requires a pair of +// increment/decrement operations to extend its life. +type RefCounted interface { + // Acquire prevents the value from being destroyed until the + // returned function is called. + Acquire() func() } -// Arg is a marker interface that can be embedded to indicate a type is -// intended for use as a Function argument. -type Arg interface{ memoizeArg() } - -// Function is the type for functions that can be memoized. -// The result must be a pointer. -type Function func(ctx context.Context, arg Arg) interface{} +// A Promise represents the future result of a call to a function. +type Promise struct { + debug string // for observability -type state int - -const ( - stateIdle = iota - stateRunning - stateCompleted - stateDestroyed -) + // refcount is the reference count in the containing Store, used by + // Store.Promise. It is guarded by Store.promisesMu on the containing Store. + refcount int32 -// Handle is returned from a store when a key is bound to a function. -// It is then used to access the results of that function. -// -// A Handle starts out in idle state, waiting for something to demand its -// evaluation. It then transitions into running state. While it's running, -// waiters tracks the number of Get calls waiting for a result, and the done -// channel is used to notify waiters of the next state transition. Once the -// evaluation finishes, value is set, state changes to completed, and done -// is closed, unblocking waiters. Alternatively, as Get calls are cancelled, -// they decrement waiters. If it drops to zero, the inner context is cancelled, -// computation is abandoned, and state resets to idle to start the process over -// again. -type Handle struct { - key interface{} - mu sync.Mutex - - // generations is the set of generations in which this handle is valid. - generations map[*Generation]struct{} + mu sync.Mutex + // A Promise starts out IDLE, waiting for something to demand + // its evaluation. It then transitions into RUNNING state. + // + // While RUNNING, waiters tracks the number of Get calls + // waiting for a result, and the done channel is used to + // notify waiters of the next state transition. Once + // evaluation finishes, value is set, state changes to + // COMPLETED, and done is closed, unblocking waiters. + // + // Alternatively, as Get calls are cancelled, they decrement + // waiters. If it drops to zero, the inner context is + // cancelled, computation is abandoned, and state resets to + // IDLE to start the process over again. state state // done is set in running state, and closed when exiting it. done chan struct{} @@ -157,230 +95,241 @@ type Handle struct { function Function // value is set in completed state. value interface{} - // cleanup, if non-nil, is used to perform any necessary clean-up on values - // produced by function. - cleanup func(interface{}) } -// Bind returns a handle for the given key and function. -// -// Each call to bind will return the same handle if it is already bound. Bind -// will always return a valid handle, creating one if needed. Each key can -// only have one handle at any given time. The value will be held at least -// until the associated generation is destroyed. Bind does not cause the value -// to be generated. +// NewPromise returns a promise for the future result of calling the +// specified function. // -// If cleanup is non-nil, it will be called on any non-nil values produced by -// function when they are no longer referenced. -func (g *Generation) Bind(key interface{}, function Function, cleanup func(interface{})) *Handle { - // panic early if the function is nil - // it would panic later anyway, but in a way that was much harder to debug +// The debug string is used to classify promises in logs and metrics. +// It should be drawn from a small set. +func NewPromise(debug string, function Function) *Promise { if function == nil { - panic("the function passed to bind must not be nil") - } - if atomic.LoadUint32(&g.destroyed) != 0 { - panic("operation on generation " + g.name + " destroyed by " + g.destroyedBy) - } - g.store.mu.Lock() - defer g.store.mu.Unlock() - h, ok := g.store.handles[key] - if !ok { - h := &Handle{ - key: key, - function: function, - generations: map[*Generation]struct{}{g: {}}, - cleanup: cleanup, - } - g.store.handles[key] = h - return h - } - h.mu.Lock() - defer h.mu.Unlock() - if _, ok := h.generations[g]; !ok { - h.generations[g] = struct{}{} - } - return h -} - -// Stats returns the number of each type of value in the store. -func (s *Store) Stats() map[reflect.Type]int { - s.mu.Lock() - defer s.mu.Unlock() - - result := map[reflect.Type]int{} - for k := range s.handles { - result[reflect.TypeOf(k)]++ + panic("nil function") } - return result -} - -// DebugOnlyIterate iterates through all live cache entries and calls f on them. -// It should only be used for debugging purposes. -func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { - s.mu.Lock() - defer s.mu.Unlock() - - for k, e := range s.handles { - var v interface{} - e.mu.Lock() - if e.state == stateCompleted { - v = e.value - } - e.mu.Unlock() - if v == nil { - continue - } - f(k, v) + return &Promise{ + debug: debug, + function: function, } } -func (g *Generation) Inherit(hs ...*Handle) { - for _, h := range hs { - if atomic.LoadUint32(&g.destroyed) != 0 { - panic("inherit on generation " + g.name + " destroyed by " + g.destroyedBy) - } +type state int - h.mu.Lock() - if h.state == stateDestroyed { - panic(fmt.Sprintf("inheriting destroyed handle %#v (type %T) into generation %v", h.key, h.key, g.name)) - } - h.generations[g] = struct{}{} - h.mu.Unlock() - } -} +const ( + stateIdle = iota // newly constructed, or last waiter was cancelled + stateRunning // start was called and not cancelled + stateCompleted // function call ran to completion +) -// Cached returns the value associated with a handle. +// Cached returns the value associated with a promise. // // It will never cause the value to be generated. // It will return the cached value, if present. -func (h *Handle) Cached(g *Generation) interface{} { - h.mu.Lock() - defer h.mu.Unlock() - if _, ok := h.generations[g]; !ok { - return nil - } - if h.state == stateCompleted { - return h.value +func (p *Promise) Cached() interface{} { + p.mu.Lock() + defer p.mu.Unlock() + if p.state == stateCompleted { + return p.value } return nil } -// Get returns the value associated with a handle. +// Get returns the value associated with a promise. +// +// All calls to Promise.Get on a given promise return the +// same result but the function is called (to completion) at most once. // // If the value is not yet ready, the underlying function will be invoked. -// If ctx is cancelled, Get returns nil. -func (h *Handle) Get(ctx context.Context, g *Generation, arg Arg) (interface{}, error) { - release := g.Acquire() - defer release() - +// +// If ctx is cancelled, Get returns (nil, Canceled). +// If all concurrent calls to Get are cancelled, the context provided +// to the function is cancelled. A later call to Get may attempt to +// call the function again. +func (p *Promise) Get(ctx context.Context, arg interface{}) (interface{}, error) { if ctx.Err() != nil { return nil, ctx.Err() } - h.mu.Lock() - if _, ok := h.generations[g]; !ok { - h.mu.Unlock() - - err := fmt.Errorf("reading key %#v: generation %v is not known", h.key, g.name) - if *panicOnDestroyed && ctx.Err() != nil { - panic(err) - } - return nil, err - } - switch h.state { + p.mu.Lock() + switch p.state { case stateIdle: - return h.run(ctx, g, arg) + return p.run(ctx, arg) case stateRunning: - return h.wait(ctx) + return p.wait(ctx) case stateCompleted: - defer h.mu.Unlock() - return h.value, nil - case stateDestroyed: - h.mu.Unlock() - err := fmt.Errorf("Get on destroyed entry %#v (type %T) in generation %v", h.key, h.key, g.name) - if *panicOnDestroyed { - panic(err) - } - return nil, err + defer p.mu.Unlock() + return p.value, nil default: panic("unknown state") } } -// run starts h.function and returns the result. h.mu must be locked. -func (h *Handle) run(ctx context.Context, g *Generation, arg Arg) (interface{}, error) { +// run starts p.function and returns the result. p.mu must be locked. +func (p *Promise) run(ctx context.Context, arg interface{}) (interface{}, error) { childCtx, cancel := context.WithCancel(xcontext.Detach(ctx)) - h.cancel = cancel - h.state = stateRunning - h.done = make(chan struct{}) - function := h.function // Read under the lock + p.cancel = cancel + p.state = stateRunning + p.done = make(chan struct{}) + function := p.function // Read under the lock + + // Make sure that the argument isn't destroyed while we're running in it. + release := func() {} + if rc, ok := arg.(RefCounted); ok { + release = rc.Acquire() + } - // Make sure that the generation isn't destroyed while we're running in it. - release := g.Acquire() go func() { - defer release() - // Just in case the function does something expensive without checking - // the context, double-check we're still alive. - if childCtx.Err() != nil { - return - } - v := function(childCtx, arg) - if childCtx.Err() != nil { - // It's possible that v was computed despite the context cancellation. In - // this case we should ensure that it is cleaned up. - if h.cleanup != nil && v != nil { - h.cleanup(v) + trace.WithRegion(childCtx, fmt.Sprintf("Promise.run %s", p.debug), func() { + defer release() + // Just in case the function does something expensive without checking + // the context, double-check we're still alive. + if childCtx.Err() != nil { + return + } + v := function(childCtx, arg) + if childCtx.Err() != nil { + return } - return - } - h.mu.Lock() - defer h.mu.Unlock() - // It's theoretically possible that the handle has been cancelled out - // of the run that started us, and then started running again since we - // checked childCtx above. Even so, that should be harmless, since each - // run should produce the same results. - if h.state != stateRunning { - // v will never be used, so ensure that it is cleaned up. - if h.cleanup != nil && v != nil { - h.cleanup(v) + p.mu.Lock() + defer p.mu.Unlock() + // It's theoretically possible that the promise has been cancelled out + // of the run that started us, and then started running again since we + // checked childCtx above. Even so, that should be harmless, since each + // run should produce the same results. + if p.state != stateRunning { + return } - return - } - // At this point v will be cleaned up whenever h is destroyed. - h.value = v - h.function = nil - h.state = stateCompleted - close(h.done) + + p.value = v + p.function = nil // aid GC + p.state = stateCompleted + close(p.done) + }) }() - return h.wait(ctx) + return p.wait(ctx) } -// wait waits for the value to be computed, or ctx to be cancelled. h.mu must be locked. -func (h *Handle) wait(ctx context.Context) (interface{}, error) { - h.waiters++ - done := h.done - h.mu.Unlock() +// wait waits for the value to be computed, or ctx to be cancelled. p.mu must be locked. +func (p *Promise) wait(ctx context.Context) (interface{}, error) { + p.waiters++ + done := p.done + p.mu.Unlock() select { case <-done: - h.mu.Lock() - defer h.mu.Unlock() - if h.state == stateCompleted { - return h.value, nil + p.mu.Lock() + defer p.mu.Unlock() + if p.state == stateCompleted { + return p.value, nil } return nil, nil case <-ctx.Done(): - h.mu.Lock() - defer h.mu.Unlock() - h.waiters-- - if h.waiters == 0 && h.state == stateRunning { - h.cancel() - close(h.done) - h.state = stateIdle - h.done = nil - h.cancel = nil + p.mu.Lock() + defer p.mu.Unlock() + p.waiters-- + if p.waiters == 0 && p.state == stateRunning { + p.cancel() + close(p.done) + p.state = stateIdle + p.done = nil + p.cancel = nil } return nil, ctx.Err() } } + +// An EvictionPolicy controls the eviction behavior of keys in a Store when +// they no longer have any references. +type EvictionPolicy int + +const ( + // ImmediatelyEvict evicts keys as soon as they no longer have references. + ImmediatelyEvict EvictionPolicy = iota + + // NeverEvict does not evict keys. + NeverEvict +) + +// A Store maps arbitrary keys to reference-counted promises. +// +// The zero value is a valid Store, though a store may also be created via +// NewStore if a custom EvictionPolicy is required. +type Store struct { + evictionPolicy EvictionPolicy + + promisesMu sync.Mutex + promises map[interface{}]*Promise +} + +// NewStore creates a new store with the given eviction policy. +func NewStore(policy EvictionPolicy) *Store { + return &Store{evictionPolicy: policy} +} + +// Promise returns a reference-counted promise for the future result of +// calling the specified function. +// +// Calls to Promise with the same key return the same promise, incrementing its +// reference count. The caller must call the returned function to decrement +// the promise's reference count when it is no longer needed. The returned +// function must not be called more than once. +// +// Once the last reference has been released, the promise is removed from the +// store. +func (store *Store) Promise(key interface{}, function Function) (*Promise, func()) { + store.promisesMu.Lock() + p, ok := store.promises[key] + if !ok { + p = NewPromise(reflect.TypeOf(key).String(), function) + if store.promises == nil { + store.promises = map[interface{}]*Promise{} + } + store.promises[key] = p + } + p.refcount++ + store.promisesMu.Unlock() + + var released int32 + release := func() { + if !atomic.CompareAndSwapInt32(&released, 0, 1) { + panic("release called more than once") + } + store.promisesMu.Lock() + + p.refcount-- + if p.refcount == 0 && store.evictionPolicy != NeverEvict { + // Inv: if p.refcount > 0, then store.promises[key] == p. + delete(store.promises, key) + } + store.promisesMu.Unlock() + } + + return p, release +} + +// Stats returns the number of each type of key in the store. +func (s *Store) Stats() map[reflect.Type]int { + result := map[reflect.Type]int{} + + s.promisesMu.Lock() + defer s.promisesMu.Unlock() + + for k := range s.promises { + result[reflect.TypeOf(k)]++ + } + return result +} + +// DebugOnlyIterate iterates through the store and, for each completed +// promise, calls f(k, v) for the map key k and function result v. It +// should only be used for debugging purposes. +func (s *Store) DebugOnlyIterate(f func(k, v interface{})) { + s.promisesMu.Lock() + defer s.promisesMu.Unlock() + + for k, p := range s.promises { + if v := p.Cached(); v != nil { + f(k, v) + } + } +} diff --git a/internal/memoize/memoize_test.go b/internal/memoize/memoize_test.go index f05966b4614..c54572d59ca 100644 --- a/internal/memoize/memoize_test.go +++ b/internal/memoize/memoize_test.go @@ -6,102 +6,161 @@ package memoize_test import ( "context" - "strings" + "sync" "testing" + "time" "golang.org/x/tools/internal/memoize" ) func TestGet(t *testing.T) { - s := &memoize.Store{} - g := s.Generation("x") + var store memoize.Store evaled := 0 - h := g.Bind("key", func(context.Context, memoize.Arg) interface{} { + h, release := store.Promise("key", func(context.Context, interface{}) interface{} { evaled++ return "res" - }, nil) - expectGet(t, h, g, "res") - expectGet(t, h, g, "res") + }) + defer release() + expectGet(t, h, "res") + expectGet(t, h, "res") if evaled != 1 { t.Errorf("got %v calls to function, wanted 1", evaled) } } -func expectGet(t *testing.T, h *memoize.Handle, g *memoize.Generation, wantV interface{}) { +func expectGet(t *testing.T, h *memoize.Promise, wantV interface{}) { t.Helper() - gotV, gotErr := h.Get(context.Background(), g, nil) + gotV, gotErr := h.Get(context.Background(), nil) if gotV != wantV || gotErr != nil { t.Fatalf("Get() = %v, %v, wanted %v, nil", gotV, gotErr, wantV) } } -func expectGetError(t *testing.T, h *memoize.Handle, g *memoize.Generation, substr string) { - gotV, gotErr := h.Get(context.Background(), g, nil) - if gotErr == nil || !strings.Contains(gotErr.Error(), substr) { - t.Fatalf("Get() = %v, %v, wanted err %q", gotV, gotErr, substr) +func TestNewPromise(t *testing.T) { + calls := 0 + f := func(context.Context, interface{}) interface{} { + calls++ + return calls } -} -func TestGenerations(t *testing.T) { - s := &memoize.Store{} - // Evaluate key in g1. - g1 := s.Generation("g1") - h1 := g1.Bind("key", func(context.Context, memoize.Arg) interface{} { return "res" }, nil) - expectGet(t, h1, g1, "res") - - // Get key in g2. It should inherit the value from g1. - g2 := s.Generation("g2") - h2 := g2.Bind("key", func(context.Context, memoize.Arg) interface{} { - t.Fatal("h2 should not need evaluation") - return "error" - }, nil) - expectGet(t, h2, g2, "res") - - // With g1 destroyed, g2 should still work. - g1.Destroy("TestGenerations") - expectGet(t, h2, g2, "res") - - // With all generations destroyed, key should be re-evaluated. - g2.Destroy("TestGenerations") - g3 := s.Generation("g3") - h3 := g3.Bind("key", func(context.Context, memoize.Arg) interface{} { return "new res" }, nil) - expectGet(t, h3, g3, "new res") + // All calls to Get on the same promise return the same result. + p1 := memoize.NewPromise("debug", f) + expectGet(t, p1, 1) + expectGet(t, p1, 1) + + // A new promise calls the function again. + p2 := memoize.NewPromise("debug", f) + expectGet(t, p2, 2) + expectGet(t, p2, 2) + + // The original promise is unchanged. + expectGet(t, p1, 1) } -func TestCleanup(t *testing.T) { - s := &memoize.Store{} - g1 := s.Generation("g1") +func TestStoredPromiseRefCounting(t *testing.T) { + var store memoize.Store v1 := false v2 := false - cleanup := func(v interface{}) { - *(v.(*bool)) = true - } - h1 := g1.Bind("key1", func(context.Context, memoize.Arg) interface{} { + p1, release1 := store.Promise("key1", func(context.Context, interface{}) interface{} { return &v1 - }, nil) - h2 := g1.Bind("key2", func(context.Context, memoize.Arg) interface{} { + }) + p2, release2 := store.Promise("key2", func(context.Context, interface{}) interface{} { return &v2 - }, cleanup) - expectGet(t, h1, g1, &v1) - expectGet(t, h2, g1, &v2) - g2 := s.Generation("g2") - g2.Inherit(h1, h2) - - g1.Destroy("TestCleanup") - expectGet(t, h1, g2, &v1) - expectGet(t, h2, g2, &v2) - for k, v := range map[string]*bool{"key1": &v1, "key2": &v2} { - if got, want := *v, false; got != want { - t.Errorf("after destroying g1, bound value %q is cleaned up", k) - } + }) + expectGet(t, p1, &v1) + expectGet(t, p2, &v2) + + expectGet(t, p1, &v1) + expectGet(t, p2, &v2) + + p2Copy, release2Copy := store.Promise("key2", func(context.Context, interface{}) interface{} { + return &v1 + }) + if p2 != p2Copy { + t.Error("Promise returned a new value while old is not destroyed yet") } - g2.Destroy("TestCleanup") + expectGet(t, p2Copy, &v2) + + release2() + if got, want := v2, false; got != want { + t.Errorf("after destroying first v2 ref, got %v, want %v", got, want) + } + release2Copy() if got, want := v1, false; got != want { - t.Error("after destroying g2, v1 is cleaned up") + t.Errorf("after destroying v2, got %v, want %v", got, want) + } + release1() + + p2Copy, release2Copy = store.Promise("key2", func(context.Context, interface{}) interface{} { + return &v2 + }) + if p2 == p2Copy { + t.Error("Promise returned previously destroyed value") + } + release2Copy() +} + +func TestPromiseDestroyedWhileRunning(t *testing.T) { + // Test that calls to Promise.Get return even if the promise is destroyed while running. + + var store memoize.Store + c := make(chan int) + + var v int + h, release := store.Promise("key", func(ctx context.Context, _ interface{}) interface{} { + <-c + <-c + if err := ctx.Err(); err != nil { + t.Errorf("ctx.Err() = %v, want nil", err) + } + return &v + }) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // arbitrary timeout; may be removed if it causes flakes + defer cancel() + + var wg sync.WaitGroup + wg.Add(1) + var got interface{} + var err error + go func() { + got, err = h.Get(ctx, nil) + wg.Done() + }() + + c <- 0 // send once to enter the promise function + release() // release before the promise function returns + c <- 0 // let the promise function proceed + + wg.Wait() + + if err != nil { + t.Errorf("Get() failed: %v", err) + } + if got != &v { + t.Errorf("Get() = %v, want %v", got, v) } - if got, want := v2, true; got != want { - t.Error("after destroying g2, v2 is not cleaned up") +} + +func TestDoubleReleasePanics(t *testing.T) { + var store memoize.Store + _, release := store.Promise("key", func(ctx context.Context, _ interface{}) interface{} { return 0 }) + + panicked := false + + func() { + defer func() { + if recover() != nil { + panicked = true + } + }() + release() + release() + }() + + if !panicked { + t.Errorf("calling release() twice did not panic") } } diff --git a/internal/persistent/map.go b/internal/persistent/map.go new file mode 100644 index 00000000000..f5dd10206b8 --- /dev/null +++ b/internal/persistent/map.go @@ -0,0 +1,297 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The persistent package defines various persistent data structures; +// that is, data structures that can be efficiently copied and modified +// in sublinear time. +package persistent + +import ( + "math/rand" + "sync/atomic" +) + +// Implementation details: +// * Each value is reference counted by nodes which hold it. +// * Each node is reference counted by its parent nodes. +// * Each map is considered a top-level parent node from reference counting perspective. +// * Each change does always effectivelly produce a new top level node. +// +// Functions which operate directly with nodes do have a notation in form of +// `foo(arg1:+n1, arg2:+n2) (ret1:+n3)`. +// Each argument is followed by a delta change to its reference counter. +// In case if no change is expected, the delta will be `-0`. + +// Map is an associative mapping from keys to values, both represented as +// interface{}. Key comparison and iteration order is defined by a +// client-provided function that implements a strict weak order. +// +// Maps can be Cloned in constant time. +// Get, Store, and Delete operations are done on average in logarithmic time. +// Maps can be Updated in O(m log(n/m)) time for maps of size n and m, where m < n. +// +// Values are reference counted, and a client-supplied release function +// is called when a value is no longer referenced by a map or any clone. +// +// Internally the implementation is based on a randomized persistent treap: +// https://en.wikipedia.org/wiki/Treap. +type Map struct { + less func(a, b interface{}) bool + root *mapNode +} + +type mapNode struct { + key interface{} + value *refValue + weight uint64 + refCount int32 + left, right *mapNode +} + +type refValue struct { + refCount int32 + value interface{} + release func(key, value interface{}) +} + +func newNodeWithRef(key, value interface{}, release func(key, value interface{})) *mapNode { + return &mapNode{ + key: key, + value: &refValue{ + value: value, + release: release, + refCount: 1, + }, + refCount: 1, + weight: rand.Uint64(), + } +} + +func (node *mapNode) shallowCloneWithRef() *mapNode { + atomic.AddInt32(&node.value.refCount, 1) + return &mapNode{ + key: node.key, + value: node.value, + weight: node.weight, + refCount: 1, + } +} + +func (node *mapNode) incref() *mapNode { + if node != nil { + atomic.AddInt32(&node.refCount, 1) + } + return node +} + +func (node *mapNode) decref() { + if node == nil { + return + } + if atomic.AddInt32(&node.refCount, -1) == 0 { + if atomic.AddInt32(&node.value.refCount, -1) == 0 { + if node.value.release != nil { + node.value.release(node.key, node.value.value) + } + node.value.value = nil + node.value.release = nil + } + node.left.decref() + node.right.decref() + } +} + +// NewMap returns a new map whose keys are ordered by the given comparison +// function (a strict weak order). It is the responsibility of the caller to +// Destroy it at later time. +func NewMap(less func(a, b interface{}) bool) *Map { + return &Map{ + less: less, + } +} + +// Clone returns a copy of the given map. It is a responsibility of the caller +// to Destroy it at later time. +func (pm *Map) Clone() *Map { + return &Map{ + less: pm.less, + root: pm.root.incref(), + } +} + +// Destroy destroys the map. +// +// After Destroy, the Map should not be used again. +func (pm *Map) Destroy() { + // The implementation of these two functions is the same, + // but their intent is different. + pm.Clear() +} + +// Clear removes all entries from the map. +func (pm *Map) Clear() { + pm.root.decref() + pm.root = nil +} + +// Range calls f sequentially in ascending key order for all entries in the map. +func (pm *Map) Range(f func(key, value interface{})) { + pm.root.forEach(f) +} + +func (node *mapNode) forEach(f func(key, value interface{})) { + if node == nil { + return + } + node.left.forEach(f) + f(node.key, node.value.value) + node.right.forEach(f) +} + +// Get returns the map value associated with the specified key, or nil if no entry +// is present. The ok result indicates whether an entry was found in the map. +func (pm *Map) Get(key interface{}) (interface{}, bool) { + node := pm.root + for node != nil { + if pm.less(key, node.key) { + node = node.left + } else if pm.less(node.key, key) { + node = node.right + } else { + return node.value.value, true + } + } + return nil, false +} + +// SetAll updates the map with key/value pairs from the other map, overwriting existing keys. +// It is equivalent to calling Set for each entry in the other map but is more efficient. +// Both maps must have the same comparison function, otherwise behavior is undefined. +func (pm *Map) SetAll(other *Map) { + root := pm.root + pm.root = union(root, other.root, pm.less, true) + root.decref() +} + +// Set updates the value associated with the specified key. +// If release is non-nil, it will be called with entry's key and value once the +// key is no longer contained in the map or any clone. +func (pm *Map) Set(key, value interface{}, release func(key, value interface{})) { + first := pm.root + second := newNodeWithRef(key, value, release) + pm.root = union(first, second, pm.less, true) + first.decref() + second.decref() +} + +// union returns a new tree which is a union of first and second one. +// If overwrite is set to true, second one would override a value for any duplicate keys. +// +// union(first:-0, second:-0) (result:+1) +// Union borrows both subtrees without affecting their refcount and returns a +// new reference that the caller is expected to call decref. +func union(first, second *mapNode, less func(a, b interface{}) bool, overwrite bool) *mapNode { + if first == nil { + return second.incref() + } + if second == nil { + return first.incref() + } + + if first.weight < second.weight { + second, first, overwrite = first, second, !overwrite + } + + left, mid, right := split(second, first.key, less, false) + var result *mapNode + if overwrite && mid != nil { + result = mid.shallowCloneWithRef() + } else { + result = first.shallowCloneWithRef() + } + result.weight = first.weight + result.left = union(first.left, left, less, overwrite) + result.right = union(first.right, right, less, overwrite) + left.decref() + mid.decref() + right.decref() + return result +} + +// split the tree midway by the key into three different ones. +// Return three new trees: left with all nodes with smaller than key, mid with +// the node matching the key, right with all nodes larger than key. +// If there are no nodes in one of trees, return nil instead of it. +// If requireMid is set (such as during deletion), then all return arguments +// are nil if mid is not found. +// +// split(n:-0) (left:+1, mid:+1, right:+1) +// Split borrows n without affecting its refcount, and returns three +// new references that that caller is expected to call decref. +func split(n *mapNode, key interface{}, less func(a, b interface{}) bool, requireMid bool) (left, mid, right *mapNode) { + if n == nil { + return nil, nil, nil + } + + if less(n.key, key) { + left, mid, right := split(n.right, key, less, requireMid) + if requireMid && mid == nil { + return nil, nil, nil + } + newN := n.shallowCloneWithRef() + newN.left = n.left.incref() + newN.right = left + return newN, mid, right + } else if less(key, n.key) { + left, mid, right := split(n.left, key, less, requireMid) + if requireMid && mid == nil { + return nil, nil, nil + } + newN := n.shallowCloneWithRef() + newN.left = right + newN.right = n.right.incref() + return left, mid, newN + } + mid = n.shallowCloneWithRef() + return n.left.incref(), mid, n.right.incref() +} + +// Delete deletes the value for a key. +func (pm *Map) Delete(key interface{}) { + root := pm.root + left, mid, right := split(root, key, pm.less, true) + if mid == nil { + return + } + pm.root = merge(left, right) + left.decref() + mid.decref() + right.decref() + root.decref() +} + +// merge two trees while preserving the weight invariant. +// All nodes in left must have smaller keys than any node in right. +// +// merge(left:-0, right:-0) (result:+1) +// Merge borrows its arguments without affecting their refcount +// and returns a new reference that the caller is expected to call decref. +func merge(left, right *mapNode) *mapNode { + switch { + case left == nil: + return right.incref() + case right == nil: + return left.incref() + case left.weight > right.weight: + root := left.shallowCloneWithRef() + root.left = left.left.incref() + root.right = merge(left.right, right) + return root + default: + root := right.shallowCloneWithRef() + root.left = merge(left, right.left) + root.right = right.right.incref() + return root + } +} diff --git a/internal/persistent/map_test.go b/internal/persistent/map_test.go new file mode 100644 index 00000000000..9f89a1d300c --- /dev/null +++ b/internal/persistent/map_test.go @@ -0,0 +1,355 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package persistent + +import ( + "fmt" + "math/rand" + "reflect" + "sync/atomic" + "testing" +) + +type mapEntry struct { + key int + value int +} + +type validatedMap struct { + impl *Map + expected map[int]int // current key-value mapping. + deleted map[mapEntry]int // maps deleted entries to their clock time of last deletion + seen map[mapEntry]int // maps seen entries to their clock time of last insertion + clock int +} + +func TestSimpleMap(t *testing.T) { + deletedEntries := make(map[mapEntry]int) + seenEntries := make(map[mapEntry]int) + + m1 := &validatedMap{ + impl: NewMap(func(a, b interface{}) bool { + return a.(int) < b.(int) + }), + expected: make(map[int]int), + deleted: deletedEntries, + seen: seenEntries, + } + + m3 := m1.clone() + validateRef(t, m1, m3) + m3.set(t, 8, 8) + validateRef(t, m1, m3) + m3.destroy() + + assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ + {key: 8, value: 8}: {}, + }) + + validateRef(t, m1) + m1.set(t, 1, 1) + validateRef(t, m1) + m1.set(t, 2, 2) + validateRef(t, m1) + m1.set(t, 3, 3) + validateRef(t, m1) + m1.remove(t, 2) + validateRef(t, m1) + m1.set(t, 6, 6) + validateRef(t, m1) + + assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ + {key: 2, value: 2}: {}, + {key: 8, value: 8}: {}, + }) + + m2 := m1.clone() + validateRef(t, m1, m2) + m1.set(t, 6, 60) + validateRef(t, m1, m2) + m1.remove(t, 1) + validateRef(t, m1, m2) + + gotAllocs := int(testing.AllocsPerRun(10, func() { + m1.impl.Delete(100) + m1.impl.Delete(1) + })) + wantAllocs := 0 + if gotAllocs != wantAllocs { + t.Errorf("wanted %d allocs, got %d", wantAllocs, gotAllocs) + } + + for i := 10; i < 14; i++ { + m1.set(t, i, i) + validateRef(t, m1, m2) + } + + m1.set(t, 10, 100) + validateRef(t, m1, m2) + + m1.remove(t, 12) + validateRef(t, m1, m2) + + m2.set(t, 4, 4) + validateRef(t, m1, m2) + m2.set(t, 5, 5) + validateRef(t, m1, m2) + + m1.destroy() + + assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ + {key: 2, value: 2}: {}, + {key: 6, value: 60}: {}, + {key: 8, value: 8}: {}, + {key: 10, value: 10}: {}, + {key: 10, value: 100}: {}, + {key: 11, value: 11}: {}, + {key: 12, value: 12}: {}, + {key: 13, value: 13}: {}, + }) + + m2.set(t, 7, 7) + validateRef(t, m2) + + m2.destroy() + + assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) +} + +func TestRandomMap(t *testing.T) { + deletedEntries := make(map[mapEntry]int) + seenEntries := make(map[mapEntry]int) + + m := &validatedMap{ + impl: NewMap(func(a, b interface{}) bool { + return a.(int) < b.(int) + }), + expected: make(map[int]int), + deleted: deletedEntries, + seen: seenEntries, + } + + keys := make([]int, 0, 1000) + for i := 0; i < 1000; i++ { + key := rand.Intn(10000) + m.set(t, key, key) + keys = append(keys, key) + + if i%10 == 1 { + index := rand.Intn(len(keys)) + last := len(keys) - 1 + key = keys[index] + keys[index], keys[last] = keys[last], keys[index] + keys = keys[:last] + + m.remove(t, key) + } + } + + m.destroy() + assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) +} + +func entrySet(m map[mapEntry]int) map[mapEntry]struct{} { + set := make(map[mapEntry]struct{}) + for k := range m { + set[k] = struct{}{} + } + return set +} + +func TestUpdate(t *testing.T) { + deletedEntries := make(map[mapEntry]int) + seenEntries := make(map[mapEntry]int) + + m1 := &validatedMap{ + impl: NewMap(func(a, b interface{}) bool { + return a.(int) < b.(int) + }), + expected: make(map[int]int), + deleted: deletedEntries, + seen: seenEntries, + } + m2 := m1.clone() + + m1.set(t, 1, 1) + m1.set(t, 2, 2) + m2.set(t, 2, 20) + m2.set(t, 3, 3) + m1.setAll(t, m2) + + m1.destroy() + m2.destroy() + assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) +} + +func validateRef(t *testing.T, maps ...*validatedMap) { + t.Helper() + + actualCountByEntry := make(map[mapEntry]int32) + nodesByEntry := make(map[mapEntry]map[*mapNode]struct{}) + expectedCountByEntry := make(map[mapEntry]int32) + for i, m := range maps { + dfsRef(m.impl.root, actualCountByEntry, nodesByEntry) + dumpMap(t, fmt.Sprintf("%d:", i), m.impl.root) + } + for entry, nodes := range nodesByEntry { + expectedCountByEntry[entry] = int32(len(nodes)) + } + assertSameMap(t, expectedCountByEntry, actualCountByEntry) +} + +func dfsRef(node *mapNode, countByEntry map[mapEntry]int32, nodesByEntry map[mapEntry]map[*mapNode]struct{}) { + if node == nil { + return + } + + entry := mapEntry{key: node.key.(int), value: node.value.value.(int)} + countByEntry[entry] = atomic.LoadInt32(&node.value.refCount) + + nodes, ok := nodesByEntry[entry] + if !ok { + nodes = make(map[*mapNode]struct{}) + nodesByEntry[entry] = nodes + } + nodes[node] = struct{}{} + + dfsRef(node.left, countByEntry, nodesByEntry) + dfsRef(node.right, countByEntry, nodesByEntry) +} + +func dumpMap(t *testing.T, prefix string, n *mapNode) { + if n == nil { + t.Logf("%s nil", prefix) + return + } + t.Logf("%s {key: %v, value: %v (ref: %v), ref: %v, weight: %v}", prefix, n.key, n.value.value, n.value.refCount, n.refCount, n.weight) + dumpMap(t, prefix+"l", n.left) + dumpMap(t, prefix+"r", n.right) +} + +func (vm *validatedMap) validate(t *testing.T) { + t.Helper() + + validateNode(t, vm.impl.root, vm.impl.less) + + // Note: this validation may not make sense if maps were constructed using + // SetAll operations. If this proves to be problematic, remove the clock, + // deleted, and seen fields. + for key, value := range vm.expected { + entry := mapEntry{key: key, value: value} + if deleteAt := vm.deleted[entry]; deleteAt > vm.seen[entry] { + t.Fatalf("entry is deleted prematurely, key: %d, value: %d", key, value) + } + } + + actualMap := make(map[int]int, len(vm.expected)) + vm.impl.Range(func(key, value interface{}) { + if other, ok := actualMap[key.(int)]; ok { + t.Fatalf("key is present twice, key: %d, first value: %d, second value: %d", key, value, other) + } + actualMap[key.(int)] = value.(int) + }) + + assertSameMap(t, actualMap, vm.expected) +} + +func validateNode(t *testing.T, node *mapNode, less func(a, b interface{}) bool) { + if node == nil { + return + } + + if node.left != nil { + if less(node.key, node.left.key) { + t.Fatalf("left child has larger key: %v vs %v", node.left.key, node.key) + } + if node.left.weight > node.weight { + t.Fatalf("left child has larger weight: %v vs %v", node.left.weight, node.weight) + } + } + + if node.right != nil { + if less(node.right.key, node.key) { + t.Fatalf("right child has smaller key: %v vs %v", node.right.key, node.key) + } + if node.right.weight > node.weight { + t.Fatalf("right child has larger weight: %v vs %v", node.right.weight, node.weight) + } + } + + validateNode(t, node.left, less) + validateNode(t, node.right, less) +} + +func (vm *validatedMap) setAll(t *testing.T, other *validatedMap) { + vm.impl.SetAll(other.impl) + + // Note: this is buggy because we are not updating vm.clock, vm.deleted, or + // vm.seen. + for key, value := range other.expected { + vm.expected[key] = value + } + vm.validate(t) +} + +func (vm *validatedMap) set(t *testing.T, key, value int) { + entry := mapEntry{key: key, value: value} + + vm.clock++ + vm.seen[entry] = vm.clock + + vm.impl.Set(key, value, func(deletedKey, deletedValue interface{}) { + if deletedKey != key || deletedValue != value { + t.Fatalf("unexpected passed in deleted entry: %v/%v, expected: %v/%v", deletedKey, deletedValue, key, value) + } + // Not safe if closure shared between two validatedMaps. + vm.deleted[entry] = vm.clock + }) + vm.expected[key] = value + vm.validate(t) + + gotValue, ok := vm.impl.Get(key) + if !ok || gotValue != value { + t.Fatalf("unexpected get result after insertion, key: %v, expected: %v, got: %v (%v)", key, value, gotValue, ok) + } +} + +func (vm *validatedMap) remove(t *testing.T, key int) { + vm.clock++ + vm.impl.Delete(key) + delete(vm.expected, key) + vm.validate(t) + + gotValue, ok := vm.impl.Get(key) + if ok { + t.Fatalf("unexpected get result after removal, key: %v, got: %v", key, gotValue) + } +} + +func (vm *validatedMap) clone() *validatedMap { + expected := make(map[int]int, len(vm.expected)) + for key, value := range vm.expected { + expected[key] = value + } + + return &validatedMap{ + impl: vm.impl.Clone(), + expected: expected, + deleted: vm.deleted, + seen: vm.seen, + } +} + +func (vm *validatedMap) destroy() { + vm.impl.Destroy() +} + +func assertSameMap(t *testing.T, map1, map2 interface{}) { + t.Helper() + + if !reflect.DeepEqual(map1, map2) { + t.Fatalf("different maps:\n%v\nvs\n%v", map1, map2) + } +} diff --git a/internal/pkgbits/codes.go b/internal/pkgbits/codes.go new file mode 100644 index 00000000000..f0cabde96eb --- /dev/null +++ b/internal/pkgbits/codes.go @@ -0,0 +1,77 @@ +// 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 pkgbits + +// A Code is an enum value that can be encoded into bitstreams. +// +// Code types are preferable for enum types, because they allow +// Decoder to detect desyncs. +type Code interface { + // Marker returns the SyncMarker for the Code's dynamic type. + Marker() SyncMarker + + // Value returns the Code's ordinal value. + Value() int +} + +// A CodeVal distinguishes among go/constant.Value encodings. +type CodeVal int + +func (c CodeVal) Marker() SyncMarker { return SyncVal } +func (c CodeVal) Value() int { return int(c) } + +// Note: These values are public and cannot be changed without +// updating the go/types importers. + +const ( + ValBool CodeVal = iota + ValString + ValInt64 + ValBigInt + ValBigRat + ValBigFloat +) + +// A CodeType distinguishes among go/types.Type encodings. +type CodeType int + +func (c CodeType) Marker() SyncMarker { return SyncType } +func (c CodeType) Value() int { return int(c) } + +// Note: These values are public and cannot be changed without +// updating the go/types importers. + +const ( + TypeBasic CodeType = iota + TypeNamed + TypePointer + TypeSlice + TypeArray + TypeChan + TypeMap + TypeSignature + TypeStruct + TypeInterface + TypeUnion + TypeTypeParam +) + +// A CodeObj distinguishes among go/types.Object encodings. +type CodeObj int + +func (c CodeObj) Marker() SyncMarker { return SyncCodeObj } +func (c CodeObj) Value() int { return int(c) } + +// Note: These values are public and cannot be changed without +// updating the go/types importers. + +const ( + ObjAlias CodeObj = iota + ObjConst + ObjType + ObjFunc + ObjVar + ObjStub +) diff --git a/internal/pkgbits/decoder.go b/internal/pkgbits/decoder.go new file mode 100644 index 00000000000..e08099c6635 --- /dev/null +++ b/internal/pkgbits/decoder.go @@ -0,0 +1,434 @@ +// 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 pkgbits + +import ( + "encoding/binary" + "fmt" + "go/constant" + "go/token" + "io" + "math/big" + "os" + "runtime" + "strings" +) + +// A PkgDecoder provides methods for decoding a package's Unified IR +// export data. +type PkgDecoder struct { + // version is the file format version. + version uint32 + + // sync indicates whether the file uses sync markers. + sync bool + + // pkgPath is the package path for the package to be decoded. + // + // TODO(mdempsky): Remove; unneeded since CL 391014. + pkgPath string + + // elemData is the full data payload of the encoded package. + // Elements are densely and contiguously packed together. + // + // The last 8 bytes of elemData are the package fingerprint. + elemData string + + // elemEnds stores the byte-offset end positions of element + // bitstreams within elemData. + // + // For example, element I's bitstream data starts at elemEnds[I-1] + // (or 0, if I==0) and ends at elemEnds[I]. + // + // Note: elemEnds is indexed by absolute indices, not + // section-relative indices. + elemEnds []uint32 + + // elemEndsEnds stores the index-offset end positions of relocation + // sections within elemEnds. + // + // For example, section K's end positions start at elemEndsEnds[K-1] + // (or 0, if K==0) and end at elemEndsEnds[K]. + elemEndsEnds [numRelocs]uint32 +} + +// PkgPath returns the package path for the package +// +// TODO(mdempsky): Remove; unneeded since CL 391014. +func (pr *PkgDecoder) PkgPath() string { return pr.pkgPath } + +// SyncMarkers reports whether pr uses sync markers. +func (pr *PkgDecoder) SyncMarkers() bool { return pr.sync } + +// NewPkgDecoder returns a PkgDecoder initialized to read the Unified +// IR export data from input. pkgPath is the package path for the +// compilation unit that produced the export data. +// +// TODO(mdempsky): Remove pkgPath parameter; unneeded since CL 391014. +func NewPkgDecoder(pkgPath, input string) PkgDecoder { + pr := PkgDecoder{ + pkgPath: pkgPath, + } + + // TODO(mdempsky): Implement direct indexing of input string to + // avoid copying the position information. + + r := strings.NewReader(input) + + assert(binary.Read(r, binary.LittleEndian, &pr.version) == nil) + + switch pr.version { + default: + panic(fmt.Errorf("unsupported version: %v", pr.version)) + case 0: + // no flags + case 1: + var flags uint32 + assert(binary.Read(r, binary.LittleEndian, &flags) == nil) + pr.sync = flags&flagSyncMarkers != 0 + } + + assert(binary.Read(r, binary.LittleEndian, pr.elemEndsEnds[:]) == nil) + + pr.elemEnds = make([]uint32, pr.elemEndsEnds[len(pr.elemEndsEnds)-1]) + assert(binary.Read(r, binary.LittleEndian, pr.elemEnds[:]) == nil) + + pos, err := r.Seek(0, io.SeekCurrent) + assert(err == nil) + + pr.elemData = input[pos:] + assert(len(pr.elemData)-8 == int(pr.elemEnds[len(pr.elemEnds)-1])) + + return pr +} + +// NumElems returns the number of elements in section k. +func (pr *PkgDecoder) NumElems(k RelocKind) int { + count := int(pr.elemEndsEnds[k]) + if k > 0 { + count -= int(pr.elemEndsEnds[k-1]) + } + return count +} + +// TotalElems returns the total number of elements across all sections. +func (pr *PkgDecoder) TotalElems() int { + return len(pr.elemEnds) +} + +// Fingerprint returns the package fingerprint. +func (pr *PkgDecoder) Fingerprint() [8]byte { + var fp [8]byte + copy(fp[:], pr.elemData[len(pr.elemData)-8:]) + return fp +} + +// AbsIdx returns the absolute index for the given (section, index) +// pair. +func (pr *PkgDecoder) AbsIdx(k RelocKind, idx Index) int { + absIdx := int(idx) + if k > 0 { + absIdx += int(pr.elemEndsEnds[k-1]) + } + if absIdx >= int(pr.elemEndsEnds[k]) { + errorf("%v:%v is out of bounds; %v", k, idx, pr.elemEndsEnds) + } + return absIdx +} + +// DataIdx returns the raw element bitstream for the given (section, +// index) pair. +func (pr *PkgDecoder) DataIdx(k RelocKind, idx Index) string { + absIdx := pr.AbsIdx(k, idx) + + var start uint32 + if absIdx > 0 { + start = pr.elemEnds[absIdx-1] + } + end := pr.elemEnds[absIdx] + + return pr.elemData[start:end] +} + +// StringIdx returns the string value for the given string index. +func (pr *PkgDecoder) StringIdx(idx Index) string { + return pr.DataIdx(RelocString, idx) +} + +// NewDecoder returns a Decoder for the given (section, index) pair, +// and decodes the given SyncMarker from the element bitstream. +func (pr *PkgDecoder) NewDecoder(k RelocKind, idx Index, marker SyncMarker) Decoder { + r := pr.NewDecoderRaw(k, idx) + r.Sync(marker) + return r +} + +// NewDecoderRaw returns a Decoder for the given (section, index) pair. +// +// Most callers should use NewDecoder instead. +func (pr *PkgDecoder) NewDecoderRaw(k RelocKind, idx Index) Decoder { + r := Decoder{ + common: pr, + k: k, + Idx: idx, + } + + // TODO(mdempsky) r.data.Reset(...) after #44505 is resolved. + r.Data = *strings.NewReader(pr.DataIdx(k, idx)) + + r.Sync(SyncRelocs) + r.Relocs = make([]RelocEnt, r.Len()) + for i := range r.Relocs { + r.Sync(SyncReloc) + r.Relocs[i] = RelocEnt{RelocKind(r.Len()), Index(r.Len())} + } + + return r +} + +// A Decoder provides methods for decoding an individual element's +// bitstream data. +type Decoder struct { + common *PkgDecoder + + Relocs []RelocEnt + Data strings.Reader + + k RelocKind + Idx Index +} + +func (r *Decoder) checkErr(err error) { + if err != nil { + errorf("unexpected decoding error: %w", err) + } +} + +func (r *Decoder) rawUvarint() uint64 { + x, err := binary.ReadUvarint(&r.Data) + r.checkErr(err) + return x +} + +func (r *Decoder) rawVarint() int64 { + ux := r.rawUvarint() + + // Zig-zag decode. + x := int64(ux >> 1) + if ux&1 != 0 { + x = ^x + } + return x +} + +func (r *Decoder) rawReloc(k RelocKind, idx int) Index { + e := r.Relocs[idx] + assert(e.Kind == k) + return e.Idx +} + +// Sync decodes a sync marker from the element bitstream and asserts +// that it matches the expected marker. +// +// If r.common.sync is false, then Sync is a no-op. +func (r *Decoder) Sync(mWant SyncMarker) { + if !r.common.sync { + return + } + + pos, _ := r.Data.Seek(0, io.SeekCurrent) + mHave := SyncMarker(r.rawUvarint()) + writerPCs := make([]int, r.rawUvarint()) + for i := range writerPCs { + writerPCs[i] = int(r.rawUvarint()) + } + + if mHave == mWant { + return + } + + // There's some tension here between printing: + // + // (1) full file paths that tools can recognize (e.g., so emacs + // hyperlinks the "file:line" text for easy navigation), or + // + // (2) short file paths that are easier for humans to read (e.g., by + // omitting redundant or irrelevant details, so it's easier to + // focus on the useful bits that remain). + // + // The current formatting favors the former, as it seems more + // helpful in practice. But perhaps the formatting could be improved + // to better address both concerns. For example, use relative file + // paths if they would be shorter, or rewrite file paths to contain + // "$GOROOT" (like objabi.AbsFile does) if tools can be taught how + // to reliably expand that again. + + fmt.Printf("export data desync: package %q, section %v, index %v, offset %v\n", r.common.pkgPath, r.k, r.Idx, pos) + + fmt.Printf("\nfound %v, written at:\n", mHave) + if len(writerPCs) == 0 { + fmt.Printf("\t[stack trace unavailable; recompile package %q with -d=syncframes]\n", r.common.pkgPath) + } + for _, pc := range writerPCs { + fmt.Printf("\t%s\n", r.common.StringIdx(r.rawReloc(RelocString, pc))) + } + + fmt.Printf("\nexpected %v, reading at:\n", mWant) + var readerPCs [32]uintptr // TODO(mdempsky): Dynamically size? + n := runtime.Callers(2, readerPCs[:]) + for _, pc := range fmtFrames(readerPCs[:n]...) { + fmt.Printf("\t%s\n", pc) + } + + // We already printed a stack trace for the reader, so now we can + // simply exit. Printing a second one with panic or base.Fatalf + // would just be noise. + os.Exit(1) +} + +// Bool decodes and returns a bool value from the element bitstream. +func (r *Decoder) Bool() bool { + r.Sync(SyncBool) + x, err := r.Data.ReadByte() + r.checkErr(err) + assert(x < 2) + return x != 0 +} + +// Int64 decodes and returns an int64 value from the element bitstream. +func (r *Decoder) Int64() int64 { + r.Sync(SyncInt64) + return r.rawVarint() +} + +// Int64 decodes and returns a uint64 value from the element bitstream. +func (r *Decoder) Uint64() uint64 { + r.Sync(SyncUint64) + return r.rawUvarint() +} + +// Len decodes and returns a non-negative int value from the element bitstream. +func (r *Decoder) Len() int { x := r.Uint64(); v := int(x); assert(uint64(v) == x); return v } + +// Int decodes and returns an int value from the element bitstream. +func (r *Decoder) Int() int { x := r.Int64(); v := int(x); assert(int64(v) == x); return v } + +// Uint decodes and returns a uint value from the element bitstream. +func (r *Decoder) Uint() uint { x := r.Uint64(); v := uint(x); assert(uint64(v) == x); return v } + +// Code decodes a Code value from the element bitstream and returns +// its ordinal value. It's the caller's responsibility to convert the +// result to an appropriate Code type. +// +// TODO(mdempsky): Ideally this method would have signature "Code[T +// Code] T" instead, but we don't allow generic methods and the +// compiler can't depend on generics yet anyway. +func (r *Decoder) Code(mark SyncMarker) int { + r.Sync(mark) + return r.Len() +} + +// Reloc decodes a relocation of expected section k from the element +// bitstream and returns an index to the referenced element. +func (r *Decoder) Reloc(k RelocKind) Index { + r.Sync(SyncUseReloc) + return r.rawReloc(k, r.Len()) +} + +// String decodes and returns a string value from the element +// bitstream. +func (r *Decoder) String() string { + r.Sync(SyncString) + return r.common.StringIdx(r.Reloc(RelocString)) +} + +// Strings decodes and returns a variable-length slice of strings from +// the element bitstream. +func (r *Decoder) Strings() []string { + res := make([]string, r.Len()) + for i := range res { + res[i] = r.String() + } + return res +} + +// Value decodes and returns a constant.Value from the element +// bitstream. +func (r *Decoder) Value() constant.Value { + r.Sync(SyncValue) + isComplex := r.Bool() + val := r.scalar() + if isComplex { + val = constant.BinaryOp(val, token.ADD, constant.MakeImag(r.scalar())) + } + return val +} + +func (r *Decoder) scalar() constant.Value { + switch tag := CodeVal(r.Code(SyncVal)); tag { + default: + panic(fmt.Errorf("unexpected scalar tag: %v", tag)) + + case ValBool: + return constant.MakeBool(r.Bool()) + case ValString: + return constant.MakeString(r.String()) + case ValInt64: + return constant.MakeInt64(r.Int64()) + case ValBigInt: + return constant.Make(r.bigInt()) + case ValBigRat: + num := r.bigInt() + denom := r.bigInt() + return constant.Make(new(big.Rat).SetFrac(num, denom)) + case ValBigFloat: + return constant.Make(r.bigFloat()) + } +} + +func (r *Decoder) bigInt() *big.Int { + v := new(big.Int).SetBytes([]byte(r.String())) + if r.Bool() { + v.Neg(v) + } + return v +} + +func (r *Decoder) bigFloat() *big.Float { + v := new(big.Float).SetPrec(512) + assert(v.UnmarshalText([]byte(r.String())) == nil) + return v +} + +// @@@ Helpers + +// TODO(mdempsky): These should probably be removed. I think they're a +// smell that the export data format is not yet quite right. + +// PeekPkgPath returns the package path for the specified package +// index. +func (pr *PkgDecoder) PeekPkgPath(idx Index) string { + r := pr.NewDecoder(RelocPkg, idx, SyncPkgDef) + path := r.String() + if path == "" { + path = pr.pkgPath + } + return path +} + +// PeekObj returns the package path, object name, and CodeObj for the +// specified object index. +func (pr *PkgDecoder) PeekObj(idx Index) (string, string, CodeObj) { + r := pr.NewDecoder(RelocName, idx, SyncObject1) + r.Sync(SyncSym) + r.Sync(SyncPkg) + path := pr.PeekPkgPath(r.Reloc(RelocPkg)) + name := r.String() + assert(name != "") + + tag := CodeObj(r.Code(SyncCodeObj)) + + return path, name, tag +} diff --git a/internal/pkgbits/doc.go b/internal/pkgbits/doc.go new file mode 100644 index 00000000000..c8a2796b5e4 --- /dev/null +++ b/internal/pkgbits/doc.go @@ -0,0 +1,32 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package pkgbits implements low-level coding abstractions for +// Unified IR's export data format. +// +// At a low-level, a package is a collection of bitstream elements. +// Each element has a "kind" and a dense, non-negative index. +// Elements can be randomly accessed given their kind and index. +// +// Individual elements are sequences of variable-length values (e.g., +// integers, booleans, strings, go/constant values, cross-references +// to other elements). Package pkgbits provides APIs for encoding and +// decoding these low-level values, but the details of mapping +// higher-level Go constructs into elements is left to higher-level +// abstractions. +// +// Elements may cross-reference each other with "relocations." For +// example, an element representing a pointer type has a relocation +// referring to the element type. +// +// Go constructs may be composed as a constellation of multiple +// elements. For example, a declared function may have one element to +// describe the object (e.g., its name, type, position), and a +// separate element to describe its function body. This allows readers +// some flexibility in efficiently seeking or re-reading data (e.g., +// inlining requires re-reading the function body for each inlined +// call, without needing to re-read the object-level details). +// +// This is a copy of internal/pkgbits in the Go implementation. +package pkgbits diff --git a/internal/pkgbits/encoder.go b/internal/pkgbits/encoder.go new file mode 100644 index 00000000000..e98e41171e1 --- /dev/null +++ b/internal/pkgbits/encoder.go @@ -0,0 +1,383 @@ +// 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 pkgbits + +import ( + "bytes" + "crypto/md5" + "encoding/binary" + "go/constant" + "io" + "math/big" + "runtime" +) + +// currentVersion is the current version number. +// +// - v0: initial prototype +// +// - v1: adds the flags uint32 word +const currentVersion uint32 = 1 + +// A PkgEncoder provides methods for encoding a package's Unified IR +// export data. +type PkgEncoder struct { + // elems holds the bitstream for previously encoded elements. + elems [numRelocs][]string + + // stringsIdx maps previously encoded strings to their index within + // the RelocString section, to allow deduplication. That is, + // elems[RelocString][stringsIdx[s]] == s (if present). + stringsIdx map[string]Index + + // syncFrames is the number of frames to write at each sync + // marker. A negative value means sync markers are omitted. + syncFrames int +} + +// SyncMarkers reports whether pw uses sync markers. +func (pw *PkgEncoder) SyncMarkers() bool { return pw.syncFrames >= 0 } + +// NewPkgEncoder returns an initialized PkgEncoder. +// +// syncFrames is the number of caller frames that should be serialized +// at Sync points. Serializing additional frames results in larger +// export data files, but can help diagnosing desync errors in +// higher-level Unified IR reader/writer code. If syncFrames is +// negative, then sync markers are omitted entirely. +func NewPkgEncoder(syncFrames int) PkgEncoder { + return PkgEncoder{ + stringsIdx: make(map[string]Index), + syncFrames: syncFrames, + } +} + +// DumpTo writes the package's encoded data to out0 and returns the +// package fingerprint. +func (pw *PkgEncoder) DumpTo(out0 io.Writer) (fingerprint [8]byte) { + h := md5.New() + out := io.MultiWriter(out0, h) + + writeUint32 := func(x uint32) { + assert(binary.Write(out, binary.LittleEndian, x) == nil) + } + + writeUint32(currentVersion) + + var flags uint32 + if pw.SyncMarkers() { + flags |= flagSyncMarkers + } + writeUint32(flags) + + // Write elemEndsEnds. + var sum uint32 + for _, elems := range &pw.elems { + sum += uint32(len(elems)) + writeUint32(sum) + } + + // Write elemEnds. + sum = 0 + for _, elems := range &pw.elems { + for _, elem := range elems { + sum += uint32(len(elem)) + writeUint32(sum) + } + } + + // Write elemData. + for _, elems := range &pw.elems { + for _, elem := range elems { + _, err := io.WriteString(out, elem) + assert(err == nil) + } + } + + // Write fingerprint. + copy(fingerprint[:], h.Sum(nil)) + _, err := out0.Write(fingerprint[:]) + assert(err == nil) + + return +} + +// StringIdx adds a string value to the strings section, if not +// already present, and returns its index. +func (pw *PkgEncoder) StringIdx(s string) Index { + if idx, ok := pw.stringsIdx[s]; ok { + assert(pw.elems[RelocString][idx] == s) + return idx + } + + idx := Index(len(pw.elems[RelocString])) + pw.elems[RelocString] = append(pw.elems[RelocString], s) + pw.stringsIdx[s] = idx + return idx +} + +// NewEncoder returns an Encoder for a new element within the given +// section, and encodes the given SyncMarker as the start of the +// element bitstream. +func (pw *PkgEncoder) NewEncoder(k RelocKind, marker SyncMarker) Encoder { + e := pw.NewEncoderRaw(k) + e.Sync(marker) + return e +} + +// NewEncoderRaw returns an Encoder for a new element within the given +// section. +// +// Most callers should use NewEncoder instead. +func (pw *PkgEncoder) NewEncoderRaw(k RelocKind) Encoder { + idx := Index(len(pw.elems[k])) + pw.elems[k] = append(pw.elems[k], "") // placeholder + + return Encoder{ + p: pw, + k: k, + Idx: idx, + } +} + +// An Encoder provides methods for encoding an individual element's +// bitstream data. +type Encoder struct { + p *PkgEncoder + + Relocs []RelocEnt + RelocMap map[RelocEnt]uint32 + Data bytes.Buffer // accumulated element bitstream data + + encodingRelocHeader bool + + k RelocKind + Idx Index // index within relocation section +} + +// Flush finalizes the element's bitstream and returns its Index. +func (w *Encoder) Flush() Index { + var sb bytes.Buffer // TODO(mdempsky): strings.Builder after #44505 is resolved + + // Backup the data so we write the relocations at the front. + var tmp bytes.Buffer + io.Copy(&tmp, &w.Data) + + // TODO(mdempsky): Consider writing these out separately so they're + // easier to strip, along with function bodies, so that we can prune + // down to just the data that's relevant to go/types. + if w.encodingRelocHeader { + panic("encodingRelocHeader already true; recursive flush?") + } + w.encodingRelocHeader = true + w.Sync(SyncRelocs) + w.Len(len(w.Relocs)) + for _, rEnt := range w.Relocs { + w.Sync(SyncReloc) + w.Len(int(rEnt.Kind)) + w.Len(int(rEnt.Idx)) + } + + io.Copy(&sb, &w.Data) + io.Copy(&sb, &tmp) + w.p.elems[w.k][w.Idx] = sb.String() + + return w.Idx +} + +func (w *Encoder) checkErr(err error) { + if err != nil { + errorf("unexpected encoding error: %v", err) + } +} + +func (w *Encoder) rawUvarint(x uint64) { + var buf [binary.MaxVarintLen64]byte + n := binary.PutUvarint(buf[:], x) + _, err := w.Data.Write(buf[:n]) + w.checkErr(err) +} + +func (w *Encoder) rawVarint(x int64) { + // Zig-zag encode. + ux := uint64(x) << 1 + if x < 0 { + ux = ^ux + } + + w.rawUvarint(ux) +} + +func (w *Encoder) rawReloc(r RelocKind, idx Index) int { + e := RelocEnt{r, idx} + if w.RelocMap != nil { + if i, ok := w.RelocMap[e]; ok { + return int(i) + } + } else { + w.RelocMap = make(map[RelocEnt]uint32) + } + + i := len(w.Relocs) + w.RelocMap[e] = uint32(i) + w.Relocs = append(w.Relocs, e) + return i +} + +func (w *Encoder) Sync(m SyncMarker) { + if !w.p.SyncMarkers() { + return + } + + // Writing out stack frame string references requires working + // relocations, but writing out the relocations themselves involves + // sync markers. To prevent infinite recursion, we simply trim the + // stack frame for sync markers within the relocation header. + var frames []string + if !w.encodingRelocHeader && w.p.syncFrames > 0 { + pcs := make([]uintptr, w.p.syncFrames) + n := runtime.Callers(2, pcs) + frames = fmtFrames(pcs[:n]...) + } + + // TODO(mdempsky): Save space by writing out stack frames as a + // linked list so we can share common stack frames. + w.rawUvarint(uint64(m)) + w.rawUvarint(uint64(len(frames))) + for _, frame := range frames { + w.rawUvarint(uint64(w.rawReloc(RelocString, w.p.StringIdx(frame)))) + } +} + +// Bool encodes and writes a bool value into the element bitstream, +// and then returns the bool value. +// +// For simple, 2-alternative encodings, the idiomatic way to call Bool +// is something like: +// +// if w.Bool(x != 0) { +// // alternative #1 +// } else { +// // alternative #2 +// } +// +// For multi-alternative encodings, use Code instead. +func (w *Encoder) Bool(b bool) bool { + w.Sync(SyncBool) + var x byte + if b { + x = 1 + } + err := w.Data.WriteByte(x) + w.checkErr(err) + return b +} + +// Int64 encodes and writes an int64 value into the element bitstream. +func (w *Encoder) Int64(x int64) { + w.Sync(SyncInt64) + w.rawVarint(x) +} + +// Uint64 encodes and writes a uint64 value into the element bitstream. +func (w *Encoder) Uint64(x uint64) { + w.Sync(SyncUint64) + w.rawUvarint(x) +} + +// Len encodes and writes a non-negative int value into the element bitstream. +func (w *Encoder) Len(x int) { assert(x >= 0); w.Uint64(uint64(x)) } + +// Int encodes and writes an int value into the element bitstream. +func (w *Encoder) Int(x int) { w.Int64(int64(x)) } + +// Len encodes and writes a uint value into the element bitstream. +func (w *Encoder) Uint(x uint) { w.Uint64(uint64(x)) } + +// Reloc encodes and writes a relocation for the given (section, +// index) pair into the element bitstream. +// +// Note: Only the index is formally written into the element +// bitstream, so bitstream decoders must know from context which +// section an encoded relocation refers to. +func (w *Encoder) Reloc(r RelocKind, idx Index) { + w.Sync(SyncUseReloc) + w.Len(w.rawReloc(r, idx)) +} + +// Code encodes and writes a Code value into the element bitstream. +func (w *Encoder) Code(c Code) { + w.Sync(c.Marker()) + w.Len(c.Value()) +} + +// String encodes and writes a string value into the element +// bitstream. +// +// Internally, strings are deduplicated by adding them to the strings +// section (if not already present), and then writing a relocation +// into the element bitstream. +func (w *Encoder) String(s string) { + w.Sync(SyncString) + w.Reloc(RelocString, w.p.StringIdx(s)) +} + +// Strings encodes and writes a variable-length slice of strings into +// the element bitstream. +func (w *Encoder) Strings(ss []string) { + w.Len(len(ss)) + for _, s := range ss { + w.String(s) + } +} + +// Value encodes and writes a constant.Value into the element +// bitstream. +func (w *Encoder) Value(val constant.Value) { + w.Sync(SyncValue) + if w.Bool(val.Kind() == constant.Complex) { + w.scalar(constant.Real(val)) + w.scalar(constant.Imag(val)) + } else { + w.scalar(val) + } +} + +func (w *Encoder) scalar(val constant.Value) { + switch v := constant.Val(val).(type) { + default: + errorf("unhandled %v (%v)", val, val.Kind()) + case bool: + w.Code(ValBool) + w.Bool(v) + case string: + w.Code(ValString) + w.String(v) + case int64: + w.Code(ValInt64) + w.Int64(v) + case *big.Int: + w.Code(ValBigInt) + w.bigInt(v) + case *big.Rat: + w.Code(ValBigRat) + w.bigInt(v.Num()) + w.bigInt(v.Denom()) + case *big.Float: + w.Code(ValBigFloat) + w.bigFloat(v) + } +} + +func (w *Encoder) bigInt(v *big.Int) { + b := v.Bytes() + w.String(string(b)) // TODO: More efficient encoding. + w.Bool(v.Sign() < 0) +} + +func (w *Encoder) bigFloat(v *big.Float) { + b := v.Append(nil, 'p', -1) + w.String(string(b)) // TODO: More efficient encoding. +} diff --git a/internal/pkgbits/flags.go b/internal/pkgbits/flags.go new file mode 100644 index 00000000000..654222745fa --- /dev/null +++ b/internal/pkgbits/flags.go @@ -0,0 +1,9 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +const ( + flagSyncMarkers = 1 << iota // file format contains sync markers +) diff --git a/internal/pkgbits/frames_go1.go b/internal/pkgbits/frames_go1.go new file mode 100644 index 00000000000..5294f6a63ed --- /dev/null +++ b/internal/pkgbits/frames_go1.go @@ -0,0 +1,21 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.7 +// +build !go1.7 + +// TODO(mdempsky): Remove after #44505 is resolved + +package pkgbits + +import "runtime" + +func walkFrames(pcs []uintptr, visit frameVisitor) { + for _, pc := range pcs { + fn := runtime.FuncForPC(pc) + file, line := fn.FileLine(pc) + + visit(file, line, fn.Name(), pc-fn.Entry()) + } +} diff --git a/internal/pkgbits/frames_go17.go b/internal/pkgbits/frames_go17.go new file mode 100644 index 00000000000..2324ae7adfe --- /dev/null +++ b/internal/pkgbits/frames_go17.go @@ -0,0 +1,28 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.7 +// +build go1.7 + +package pkgbits + +import "runtime" + +// walkFrames calls visit for each call frame represented by pcs. +// +// pcs should be a slice of PCs, as returned by runtime.Callers. +func walkFrames(pcs []uintptr, visit frameVisitor) { + if len(pcs) == 0 { + return + } + + frames := runtime.CallersFrames(pcs) + for { + frame, more := frames.Next() + visit(frame.File, frame.Line, frame.Function, frame.PC-frame.Entry) + if !more { + return + } + } +} diff --git a/internal/pkgbits/reloc.go b/internal/pkgbits/reloc.go new file mode 100644 index 00000000000..fcdfb97ca99 --- /dev/null +++ b/internal/pkgbits/reloc.go @@ -0,0 +1,42 @@ +// 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 pkgbits + +// A RelocKind indicates a particular section within a unified IR export. +type RelocKind int32 + +// An Index represents a bitstream element index within a particular +// section. +type Index int32 + +// A relocEnt (relocation entry) is an entry in an element's local +// reference table. +// +// TODO(mdempsky): Rename this too. +type RelocEnt struct { + Kind RelocKind + Idx Index +} + +// Reserved indices within the meta relocation section. +const ( + PublicRootIdx Index = 0 + PrivateRootIdx Index = 1 +) + +const ( + RelocString RelocKind = iota + RelocMeta + RelocPosBase + RelocPkg + RelocName + RelocType + RelocObj + RelocObjExt + RelocObjDict + RelocBody + + numRelocs = iota +) diff --git a/internal/pkgbits/support.go b/internal/pkgbits/support.go new file mode 100644 index 00000000000..ad26d3b28ca --- /dev/null +++ b/internal/pkgbits/support.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pkgbits + +import "fmt" + +func assert(b bool) { + if !b { + panic("assertion failed") + } +} + +func errorf(format string, args ...interface{}) { + panic(fmt.Errorf(format, args...)) +} diff --git a/internal/pkgbits/sync.go b/internal/pkgbits/sync.go new file mode 100644 index 00000000000..5bd51ef7170 --- /dev/null +++ b/internal/pkgbits/sync.go @@ -0,0 +1,113 @@ +// 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 pkgbits + +import ( + "fmt" + "strings" +) + +// fmtFrames formats a backtrace for reporting reader/writer desyncs. +func fmtFrames(pcs ...uintptr) []string { + res := make([]string, 0, len(pcs)) + walkFrames(pcs, func(file string, line int, name string, offset uintptr) { + // Trim package from function name. It's just redundant noise. + name = strings.TrimPrefix(name, "cmd/compile/internal/noder.") + + res = append(res, fmt.Sprintf("%s:%v: %s +0x%v", file, line, name, offset)) + }) + return res +} + +type frameVisitor func(file string, line int, name string, offset uintptr) + +// SyncMarker is an enum type that represents markers that may be +// written to export data to ensure the reader and writer stay +// synchronized. +type SyncMarker int + +//go:generate stringer -type=SyncMarker -trimprefix=Sync + +const ( + _ SyncMarker = iota + + // Public markers (known to go/types importers). + + // Low-level coding markers. + SyncEOF + SyncBool + SyncInt64 + SyncUint64 + SyncString + SyncValue + SyncVal + SyncRelocs + SyncReloc + SyncUseReloc + + // Higher-level object and type markers. + SyncPublic + SyncPos + SyncPosBase + SyncObject + SyncObject1 + SyncPkg + SyncPkgDef + SyncMethod + SyncType + SyncTypeIdx + SyncTypeParamNames + SyncSignature + SyncParams + SyncParam + SyncCodeObj + SyncSym + SyncLocalIdent + SyncSelector + + // Private markers (only known to cmd/compile). + SyncPrivate + + SyncFuncExt + SyncVarExt + SyncTypeExt + SyncPragma + + SyncExprList + SyncExprs + SyncExpr + SyncExprType + SyncAssign + SyncOp + SyncFuncLit + SyncCompLit + + SyncDecl + SyncFuncBody + SyncOpenScope + SyncCloseScope + SyncCloseAnotherScope + SyncDeclNames + SyncDeclName + + SyncStmts + SyncBlockStmt + SyncIfStmt + SyncForStmt + SyncSwitchStmt + SyncRangeStmt + SyncCaseClause + SyncCommClause + SyncSelectStmt + SyncDecls + SyncLabeledStmt + SyncUseObjLocal + SyncAddLocal + SyncLinkname + SyncStmt1 + SyncStmtsEnd + SyncLabel + SyncOptLabel +) diff --git a/internal/pkgbits/syncmarker_string.go b/internal/pkgbits/syncmarker_string.go new file mode 100644 index 00000000000..4a5b0ca5f2f --- /dev/null +++ b/internal/pkgbits/syncmarker_string.go @@ -0,0 +1,89 @@ +// Code generated by "stringer -type=SyncMarker -trimprefix=Sync"; DO NOT EDIT. + +package pkgbits + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[SyncEOF-1] + _ = x[SyncBool-2] + _ = x[SyncInt64-3] + _ = x[SyncUint64-4] + _ = x[SyncString-5] + _ = x[SyncValue-6] + _ = x[SyncVal-7] + _ = x[SyncRelocs-8] + _ = x[SyncReloc-9] + _ = x[SyncUseReloc-10] + _ = x[SyncPublic-11] + _ = x[SyncPos-12] + _ = x[SyncPosBase-13] + _ = x[SyncObject-14] + _ = x[SyncObject1-15] + _ = x[SyncPkg-16] + _ = x[SyncPkgDef-17] + _ = x[SyncMethod-18] + _ = x[SyncType-19] + _ = x[SyncTypeIdx-20] + _ = x[SyncTypeParamNames-21] + _ = x[SyncSignature-22] + _ = x[SyncParams-23] + _ = x[SyncParam-24] + _ = x[SyncCodeObj-25] + _ = x[SyncSym-26] + _ = x[SyncLocalIdent-27] + _ = x[SyncSelector-28] + _ = x[SyncPrivate-29] + _ = x[SyncFuncExt-30] + _ = x[SyncVarExt-31] + _ = x[SyncTypeExt-32] + _ = x[SyncPragma-33] + _ = x[SyncExprList-34] + _ = x[SyncExprs-35] + _ = x[SyncExpr-36] + _ = x[SyncExprType-37] + _ = x[SyncAssign-38] + _ = x[SyncOp-39] + _ = x[SyncFuncLit-40] + _ = x[SyncCompLit-41] + _ = x[SyncDecl-42] + _ = x[SyncFuncBody-43] + _ = x[SyncOpenScope-44] + _ = x[SyncCloseScope-45] + _ = x[SyncCloseAnotherScope-46] + _ = x[SyncDeclNames-47] + _ = x[SyncDeclName-48] + _ = x[SyncStmts-49] + _ = x[SyncBlockStmt-50] + _ = x[SyncIfStmt-51] + _ = x[SyncForStmt-52] + _ = x[SyncSwitchStmt-53] + _ = x[SyncRangeStmt-54] + _ = x[SyncCaseClause-55] + _ = x[SyncCommClause-56] + _ = x[SyncSelectStmt-57] + _ = x[SyncDecls-58] + _ = x[SyncLabeledStmt-59] + _ = x[SyncUseObjLocal-60] + _ = x[SyncAddLocal-61] + _ = x[SyncLinkname-62] + _ = x[SyncStmt1-63] + _ = x[SyncStmtsEnd-64] + _ = x[SyncLabel-65] + _ = x[SyncOptLabel-66] +} + +const _SyncMarker_name = "EOFBoolInt64Uint64StringValueValRelocsRelocUseRelocPublicPosPosBaseObjectObject1PkgPkgDefMethodTypeTypeIdxTypeParamNamesSignatureParamsParamCodeObjSymLocalIdentSelectorPrivateFuncExtVarExtTypeExtPragmaExprListExprsExprExprTypeAssignOpFuncLitCompLitDeclFuncBodyOpenScopeCloseScopeCloseAnotherScopeDeclNamesDeclNameStmtsBlockStmtIfStmtForStmtSwitchStmtRangeStmtCaseClauseCommClauseSelectStmtDeclsLabeledStmtUseObjLocalAddLocalLinknameStmt1StmtsEndLabelOptLabel" + +var _SyncMarker_index = [...]uint16{0, 3, 7, 12, 18, 24, 29, 32, 38, 43, 51, 57, 60, 67, 73, 80, 83, 89, 95, 99, 106, 120, 129, 135, 140, 147, 150, 160, 168, 175, 182, 188, 195, 201, 209, 214, 218, 226, 232, 234, 241, 248, 252, 260, 269, 279, 296, 305, 313, 318, 327, 333, 340, 350, 359, 369, 379, 389, 394, 405, 416, 424, 432, 437, 445, 450, 458} + +func (i SyncMarker) String() string { + i -= 1 + if i < 0 || i >= SyncMarker(len(_SyncMarker_index)-1) { + return "SyncMarker(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _SyncMarker_name[_SyncMarker_index[i]:_SyncMarker_index[i+1]] +} diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index bfadb44be65..f606cb71543 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -16,8 +16,11 @@ import ( "runtime/debug" "strings" "sync" + "testing" "time" + "golang.org/x/tools/internal/goroot" + exec "golang.org/x/sys/execabs" ) @@ -329,3 +332,20 @@ func Deadline(t Testing) (time.Time, bool) { } return td.Deadline() } + +// WriteImportcfg writes an importcfg file used by the compiler or linker to +// dstPath containing entries for the packages in std and cmd in addition +// to the package to package file mappings in additionalPackageFiles. +func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) { + importcfg, err := goroot.Importcfg() + for k, v := range additionalPackageFiles { + importcfg += fmt.Sprintf("\npackagefile %s=%s", k, v) + } + if err != nil { + t.Fatalf("preparing the importcfg failed: %s", err) + } + ioutil.WriteFile(dstPath, []byte(importcfg), 0655) + if err != nil { + t.Fatalf("writing the importcfg failed: %s", err) + } +} diff --git a/internal/typeparams/normalize_test.go b/internal/typeparams/normalize_test.go index 5969eee3de0..769433d701d 100644 --- a/internal/typeparams/normalize_test.go +++ b/internal/typeparams/normalize_test.go @@ -9,6 +9,7 @@ import ( "go/parser" "go/token" "go/types" + "regexp" "strings" "testing" @@ -38,7 +39,7 @@ func TestStructuralTerms(t *testing.T) { {"package emptyintersection; type T[P interface{ ~int; string }] int", "", "empty type set"}, {"package embedded0; type T[P interface{ I }] int; type I interface { int }", "int", ""}, - {"package embedded1; type T[P interface{ I | string }] int; type I interface{ int | ~string }", "int|~string", ""}, + {"package embedded1; type T[P interface{ I | string }] int; type I interface{ int | ~string }", "int ?\\| ?~string", ""}, {"package embedded2; type T[P interface{ I; string }] int; type I interface{ int | ~string }", "string", ""}, {"package named; type T[P C] int; type C interface{ ~int|int }", "~int", ""}, @@ -52,7 +53,7 @@ type B interface{ int|string } type C interface { ~string|~int } type T[P interface{ A|B; C }] int -`, "~string|int", ""}, +`, "~string ?\\| ?int", ""}, } for _, test := range tests { @@ -96,7 +97,8 @@ type T[P interface{ A|B; C }] int qf := types.RelativeTo(pkg) got = types.TypeString(NewUnion(terms), qf) } - if got != test.want { + want := regexp.MustCompile(test.want) + if !want.MatchString(got) { t.Errorf("StructuralTerms(%s) = %q, want %q", T, got, test.want) } }) diff --git a/present/args.go b/present/args.go index d63196e028c..b4f7503b6da 100644 --- a/present/args.go +++ b/present/args.go @@ -18,7 +18,7 @@ import ( // regular expressions. That is the only change to the code from codewalk.go. // See http://9p.io/sys/doc/sam/sam.html Table II for details on the syntax. -// addrToByte evaluates the given address starting at offset start in data. +// addrToByteRange evaluates the given address starting at offset start in data. // It returns the lo and hi byte offset of the matched region within data. func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) { if addr == "" { diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index ff4212b7645..6b4d5284aec 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go @@ -10,10 +10,7 @@ // // THIS PACKAGE IS EXPERIMENTAL AND MAY CHANGE AT ANY TIME. // -// It is provided only for the gorename tool. Ideally this -// functionality will become part of the type-checker in due course, -// since it is computing it anyway, and it is robust for ill-typed -// inputs, which this package is not. +// It is provided only for the gopls tool. It requires well-typed inputs. package satisfy // import "golang.org/x/tools/refactor/satisfy" // NOTES: @@ -25,9 +22,6 @@ package satisfy // import "golang.org/x/tools/refactor/satisfy" // ... // }}) // -// TODO(adonovan): make this robust against ill-typed input. -// Or move it into the type-checker. -// // Assignability conversions are possible in the following places: // - in assignments y = x, y := x, var y = x. // - from call argument types to formal parameter types @@ -51,11 +45,15 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typeparams" ) // A Constraint records the fact that the RHS type does and must // satisfy the LHS type, which is an interface. // The names are suggestive of an assignment statement LHS = RHS. +// +// The constraint is implicitly universally quantified over any type +// parameters appearing within the two types. type Constraint struct { LHS, RHS types.Type } @@ -129,13 +127,13 @@ func (f *Finder) exprN(e ast.Expr) types.Type { case *ast.CallExpr: // x, err := f(args) - sig := f.expr(e.Fun).Underlying().(*types.Signature) + sig := coreType(f.expr(e.Fun)).(*types.Signature) f.call(sig, e.Args) case *ast.IndexExpr: // y, ok := x[i] x := f.expr(e.X) - f.assign(f.expr(e.Index), x.Underlying().(*types.Map).Key()) + f.assign(f.expr(e.Index), coreType(x).(*types.Map).Key()) case *ast.TypeAssertExpr: // y, ok := x.(T) @@ -200,7 +198,8 @@ func (f *Finder) call(sig *types.Signature, args []ast.Expr) { } } -func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Expr, T types.Type) types.Type { +// builtin visits the arguments of a builtin type with signature sig. +func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Expr) { switch obj.Name() { case "make", "new": // skip the type operand @@ -215,7 +214,7 @@ func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Ex f.expr(args[1]) } else { // append(x, y, z) - tElem := s.Underlying().(*types.Slice).Elem() + tElem := coreType(s).(*types.Slice).Elem() for _, arg := range args[1:] { f.assign(tElem, f.expr(arg)) } @@ -224,14 +223,12 @@ func (f *Finder) builtin(obj *types.Builtin, sig *types.Signature, args []ast.Ex case "delete": m := f.expr(args[0]) k := f.expr(args[1]) - f.assign(m.Underlying().(*types.Map).Key(), k) + f.assign(coreType(m).(*types.Map).Key(), k) default: // ordinary call f.call(sig, args) } - - return T } func (f *Finder) extract(tuple types.Type, i int) types.Type { @@ -358,6 +355,7 @@ func (f *Finder) expr(e ast.Expr) types.Type { f.sig = saved case *ast.CompositeLit: + // No need for coreType here: go1.18 disallows P{...} for type param P. switch T := deref(tv.Type).Underlying().(type) { case *types.Struct: for i, elem := range e.Elts { @@ -403,12 +401,20 @@ func (f *Finder) expr(e ast.Expr) types.Type { } case *ast.IndexExpr: - x := f.expr(e.X) - i := f.expr(e.Index) - if ux, ok := x.Underlying().(*types.Map); ok { - f.assign(ux.Key(), i) + if instance(f.info, e.X) { + // f[T] or C[T] -- generic instantiation + } else { + // x[i] or m[k] -- index or lookup operation + x := f.expr(e.X) + i := f.expr(e.Index) + if ux, ok := coreType(x).(*types.Map); ok { + f.assign(ux.Key(), i) + } } + case *typeparams.IndexListExpr: + // f[X, Y] -- generic instantiation + case *ast.SliceExpr: f.expr(e.X) if e.Low != nil { @@ -432,14 +438,29 @@ func (f *Finder) expr(e ast.Expr) types.Type { f.assign(tvFun.Type, arg0) } else { // function call + + // unsafe call. Treat calls to functions in unsafe like ordinary calls, + // except that their signature cannot be determined by their func obj. + // Without this special handling, f.expr(e.Fun) would fail below. + if s, ok := unparen(e.Fun).(*ast.SelectorExpr); ok { + if obj, ok := f.info.Uses[s.Sel].(*types.Builtin); ok && obj.Pkg().Path() == "unsafe" { + sig := f.info.Types[e.Fun].Type.(*types.Signature) + f.call(sig, e.Args) + return tv.Type + } + } + + // builtin call if id, ok := unparen(e.Fun).(*ast.Ident); ok { if obj, ok := f.info.Uses[id].(*types.Builtin); ok { sig := f.info.Types[id].Type.(*types.Signature) - return f.builtin(obj, sig, e.Args, tv.Type) + f.builtin(obj, sig, e.Args) + return tv.Type } } + // ordinary call - f.call(f.expr(e.Fun).Underlying().(*types.Signature), e.Args) + f.call(coreType(f.expr(e.Fun)).(*types.Signature), e.Args) } case *ast.StarExpr: @@ -499,7 +520,7 @@ func (f *Finder) stmt(s ast.Stmt) { case *ast.SendStmt: ch := f.expr(s.Chan) val := f.expr(s.Value) - f.assign(ch.Underlying().(*types.Chan).Elem(), val) + f.assign(coreType(ch).(*types.Chan).Elem(), val) case *ast.IncDecStmt: f.expr(s.X) @@ -647,35 +668,35 @@ func (f *Finder) stmt(s ast.Stmt) { if s.Key != nil { k := f.expr(s.Key) var xelem types.Type - // keys of array, *array, slice, string aren't interesting - switch ux := x.Underlying().(type) { + // Keys of array, *array, slice, string aren't interesting + // since the RHS key type is just an int. + switch ux := coreType(x).(type) { case *types.Chan: xelem = ux.Elem() case *types.Map: xelem = ux.Key() } if xelem != nil { - f.assign(xelem, k) + f.assign(k, xelem) } } if s.Value != nil { val := f.expr(s.Value) var xelem types.Type - // values of strings aren't interesting - switch ux := x.Underlying().(type) { + // Values of type strings aren't interesting because + // the RHS value type is just a rune. + switch ux := coreType(x).(type) { case *types.Array: xelem = ux.Elem() - case *types.Chan: - xelem = ux.Elem() case *types.Map: xelem = ux.Elem() case *types.Pointer: // *array - xelem = deref(ux).(*types.Array).Elem() + xelem = coreType(deref(ux)).(*types.Array).Elem() case *types.Slice: xelem = ux.Elem() } if xelem != nil { - f.assign(xelem, val) + f.assign(val, xelem) } } } @@ -690,7 +711,7 @@ func (f *Finder) stmt(s ast.Stmt) { // deref returns a pointer's element type; otherwise it returns typ. func deref(typ types.Type) types.Type { - if p, ok := typ.Underlying().(*types.Pointer); ok { + if p, ok := coreType(typ).(*types.Pointer); ok { return p.Elem() } return typ @@ -699,3 +720,19 @@ func deref(typ types.Type) types.Type { func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } func isInterface(T types.Type) bool { return types.IsInterface(T) } + +func coreType(T types.Type) types.Type { return typeparams.CoreType(T) } + +func instance(info *types.Info, expr ast.Expr) bool { + var id *ast.Ident + switch x := expr.(type) { + case *ast.Ident: + id = x + case *ast.SelectorExpr: + id = x.Sel + default: + return false + } + _, ok := typeparams.GetInstances(info)[id] + return ok +} diff --git a/refactor/satisfy/find_test.go b/refactor/satisfy/find_test.go new file mode 100644 index 00000000000..35a1e87caf4 --- /dev/null +++ b/refactor/satisfy/find_test.go @@ -0,0 +1,238 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package satisfy_test + +import ( + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "reflect" + "sort" + "testing" + + "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/refactor/satisfy" +) + +// This test exercises various operations on core types of type parameters. +// (It also provides pretty decent coverage of the non-generic operations.) +func TestGenericCoreOperations(t *testing.T) { + if !typeparams.Enabled { + t.Skip("!typeparams.Enabled") + } + + const src = `package foo + +import "unsafe" + +type I interface { f() } + +type impl struct{} +func (impl) f() {} + +// A big pile of single-serving types that implement I. +type A struct{impl} +type B struct{impl} +type C struct{impl} +type D struct{impl} +type E struct{impl} +type F struct{impl} +type G struct{impl} +type H struct{impl} +type J struct{impl} +type K struct{impl} +type L struct{impl} +type M struct{impl} +type N struct{impl} +type O struct{impl} +type P struct{impl} +type Q struct{impl} +type R struct{impl} +type S struct{impl} +type T struct{impl} +type U struct{impl} +type V struct{impl} + +type Generic[T any] struct{impl} +func (Generic[T]) g(T) {} + +type GI[T any] interface{ + g(T) +} + +func _[Slice interface{ []I }](s Slice) Slice { + s[0] = L{} // I <- L + return append(s, A{}) // I <- A +} + +func _[Func interface{ func(I) B }](fn Func) { + b := fn(C{}) // I <- C + var _ I = b // I <- B +} + +func _[Chan interface{ chan D }](ch Chan) { + var i I + for i = range ch {} // I <- D + _ = i +} + +func _[Chan interface{ chan E }](ch Chan) { + var _ I = <-ch // I <- E +} + +func _[Chan interface{ chan I }](ch Chan) { + ch <- F{} // I <- F +} + +func _[Map interface{ map[G]H }](m Map) { + var k, v I + for k, v = range m {} // I <- G, I <- H + _, _ = k, v +} + +func _[Map interface{ map[I]K }](m Map) { + var _ I = m[J{}] // I <- J, I <- K + delete(m, R{}) // I <- R + _, _ = m[J{}] +} + +func _[Array interface{ [1]I }](a Array) { + a[0] = M{} // I <- M +} + +func _[Array interface{ [1]N }](a Array) { + var _ I = a[0] // I <- N +} + +func _[Array interface{ [1]O }](a Array) { + var v I + for _, v = range a {} // I <- O + _ = v +} + +func _[ArrayPtr interface{ *[1]P }](a ArrayPtr) { + var v I + for _, v = range a {} // I <- P + _ = v +} + +func _[Slice interface{ []Q }](s Slice) { + var v I + for _, v = range s {} // I <- Q + _ = v +} + +func _[Func interface{ func() (S, bool) }](fn Func) { + var i I + i, _ = fn() // I <- S + _ = i +} + +func _() I { + var _ I = T{} // I <- T + var _ I = Generic[T]{} // I <- Generic[T] + var _ I = Generic[string]{} // I <- Generic[string] + return U{} // I <- U +} + +var _ GI[string] = Generic[string]{} // GI[string] <- Generic[string] + +// universally quantified constraints: +// the type parameter may appear on the left, the right, or both sides. + +func _[T any](g Generic[T]) GI[T] { + return g // GI[T] <- Generic[T] +} + +func _[T any]() { + type GI2[T any] interface{ g(string) } + var _ GI2[T] = Generic[string]{} // GI2[T] <- Generic[string] +} + +type Gen2[T any] struct{} +func (f Gen2[T]) g(string) { global = f } // GI[string] <- Gen2[T] + +var global GI[string] + +func _() { + var x [3]V + // golang/go#56227: the finder should visit calls in the unsafe package. + _ = unsafe.Slice(&x[0], func() int { var _ I = x[0]; return 3 }()) // I <- V +} +` + got := constraints(t, src) + want := []string{ + "p.GI2[T] <- p.Generic[string]", // implicitly "forall T" quantified + "p.GI[T] <- p.Generic[T]", // implicitly "forall T" quantified + "p.GI[string] <- p.Gen2[T]", // implicitly "forall T" quantified + "p.GI[string] <- p.Generic[string]", + "p.I <- p.A", + "p.I <- p.B", + "p.I <- p.C", + "p.I <- p.D", + "p.I <- p.E", + "p.I <- p.F", + "p.I <- p.G", + "p.I <- p.Generic[p.T]", + "p.I <- p.Generic[string]", + "p.I <- p.H", + "p.I <- p.J", + "p.I <- p.K", + "p.I <- p.L", + "p.I <- p.M", + "p.I <- p.N", + "p.I <- p.O", + "p.I <- p.P", + "p.I <- p.Q", + "p.I <- p.R", + "p.I <- p.S", + "p.I <- p.T", + "p.I <- p.U", + "p.I <- p.V", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("found unexpected constraints: got %s, want %s", got, want) + } +} + +func constraints(t *testing.T, src string) []string { + // parse + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", src, 0) + if err != nil { + t.Fatal(err) // parse error + } + files := []*ast.File{f} + + // type-check + info := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Scopes: make(map[ast.Node]*types.Scope), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + } + typeparams.InitInstanceInfo(info) + conf := types.Config{ + Importer: importer.Default(), + } + if _, err := conf.Check("p", fset, files, info); err != nil { + t.Fatal(err) // type error + } + + // gather constraints + var finder satisfy.Finder + finder.Find(info, files) + var constraints []string + for c := range finder.Result { + constraints = append(constraints, fmt.Sprintf("%v <- %v", c.LHS, c.RHS)) + } + sort.Strings(constraints) + return constraints +}