diff --git a/gopls/internal/cmd/workspace_symbol.go b/gopls/internal/cmd/workspace_symbol.go
index f41a85f6466..9fa7526a24d 100644
--- a/gopls/internal/cmd/workspace_symbol.go
+++ b/gopls/internal/cmd/workspace_symbol.go
@@ -10,7 +10,7 @@ import (
"fmt"
"strings"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/tool"
)
diff --git a/gopls/internal/debug/info.go b/gopls/internal/debug/info.go
index 84027ec43e1..b2824d86f38 100644
--- a/gopls/internal/debug/info.go
+++ b/gopls/internal/debug/info.go
@@ -14,6 +14,8 @@ import (
"runtime"
"runtime/debug"
"strings"
+
+ "golang.org/x/tools/gopls/internal/version"
)
type PrintMode int
@@ -25,16 +27,6 @@ const (
JSON
)
-// Version is a manually-updated mechanism for tracking versions.
-func Version() string {
- if info, ok := debug.ReadBuildInfo(); ok {
- if info.Main.Version != "" {
- return info.Main.Version
- }
- }
- return "(unknown)"
-}
-
// ServerVersion is the format used by gopls to report its version to the
// client. This format is structured so that the client can parse it easily.
type ServerVersion struct {
@@ -48,12 +40,12 @@ type ServerVersion struct {
func VersionInfo() *ServerVersion {
if info, ok := debug.ReadBuildInfo(); ok {
return &ServerVersion{
- Version: Version(),
+ Version: version.Version(),
BuildInfo: info,
}
}
return &ServerVersion{
- Version: Version(),
+ Version: version.Version(),
BuildInfo: &debug.BuildInfo{
Path: "gopls, built in GOPATH mode",
GoVersion: runtime.Version(),
@@ -63,11 +55,12 @@ func VersionInfo() *ServerVersion {
// PrintServerInfo writes HTML debug info to w for the Instance.
func (i *Instance) PrintServerInfo(ctx context.Context, w io.Writer) {
+ workDir, _ := os.Getwd()
section(w, HTML, "Server Instance", func() {
fmt.Fprintf(w, "Start time: %v\n", i.StartTime)
fmt.Fprintf(w, "LogFile: %s\n", i.Logfile)
fmt.Fprintf(w, "pid: %d\n", os.Getpid())
- fmt.Fprintf(w, "Working directory: %s\n", i.Workdir)
+ fmt.Fprintf(w, "Working directory: %s\n", workDir)
fmt.Fprintf(w, "Address: %s\n", i.ServerAddress)
fmt.Fprintf(w, "Debug address: %s\n", i.DebugAddress())
})
@@ -123,11 +116,11 @@ func section(w io.Writer, mode PrintMode, title string, body func()) {
}
func printBuildInfo(w io.Writer, info *ServerVersion, verbose bool, mode PrintMode) {
- fmt.Fprintf(w, "%v %v\n", info.Path, Version())
- printModuleInfo(w, info.Main, mode)
+ fmt.Fprintf(w, "%v %v\n", info.Path, version.Version())
if !verbose {
return
}
+ printModuleInfo(w, info.Main, mode)
for _, dep := range info.Deps {
printModuleInfo(w, *dep, mode)
}
diff --git a/gopls/internal/debug/info_test.go b/gopls/internal/debug/info_test.go
index 3bc9290c157..7f24b696682 100644
--- a/gopls/internal/debug/info_test.go
+++ b/gopls/internal/debug/info_test.go
@@ -11,6 +11,8 @@ import (
"encoding/json"
"runtime"
"testing"
+
+ "golang.org/x/tools/gopls/internal/version"
)
func TestPrintVersionInfoJSON(t *testing.T) {
@@ -27,7 +29,7 @@ func TestPrintVersionInfoJSON(t *testing.T) {
if g, w := got.GoVersion, runtime.Version(); g != w {
t.Errorf("go version = %v, want %v", g, w)
}
- if g, w := got.Version, Version(); g != w {
+ if g, w := got.Version, version.Version(); g != w {
t.Errorf("gopls version = %v, want %v", g, w)
}
// Other fields of BuildInfo may not be available during test.
@@ -41,7 +43,7 @@ func TestPrintVersionInfoPlainText(t *testing.T) {
res := buf.Bytes()
// Other fields of BuildInfo may not be available during test.
- wantGoplsVersion, wantGoVersion := Version(), runtime.Version()
+ wantGoplsVersion, wantGoVersion := version.Version(), runtime.Version()
if !bytes.Contains(res, []byte(wantGoplsVersion)) || !bytes.Contains(res, []byte(wantGoVersion)) {
t.Errorf("plaintext output = %q,\nwant (version: %v, go: %v)", res, wantGoplsVersion, wantGoVersion)
}
diff --git a/gopls/internal/debug/rpc.go b/gopls/internal/debug/rpc.go
index 5610021479c..0fee0f4a435 100644
--- a/gopls/internal/debug/rpc.go
+++ b/gopls/internal/debug/rpc.go
@@ -84,19 +84,19 @@ func (r *Rpcs) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) co
defer r.mu.Unlock()
switch {
case event.IsStart(ev):
- if _, stats := r.getRPCSpan(ctx, ev); stats != nil {
+ if _, stats := r.getRPCSpan(ctx); stats != nil {
stats.Started++
}
case event.IsEnd(ev):
- span, stats := r.getRPCSpan(ctx, ev)
+ span, stats := r.getRPCSpan(ctx)
if stats != nil {
- endRPC(ctx, ev, span, stats)
+ endRPC(span, stats)
}
case event.IsMetric(ev):
sent := byteUnits(tag.SentBytes.Get(lm))
rec := byteUnits(tag.ReceivedBytes.Get(lm))
if sent != 0 || rec != 0 {
- if _, stats := r.getRPCSpan(ctx, ev); stats != nil {
+ if _, stats := r.getRPCSpan(ctx); stats != nil {
stats.Sent += sent
stats.Received += rec
}
@@ -105,7 +105,7 @@ func (r *Rpcs) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) co
return ctx
}
-func endRPC(ctx context.Context, ev core.Event, span *export.Span, stats *rpcStats) {
+func endRPC(span *export.Span, stats *rpcStats) {
// update the basic counts
stats.Completed++
@@ -152,7 +152,7 @@ func endRPC(ctx context.Context, ev core.Event, span *export.Span, stats *rpcSta
}
}
-func (r *Rpcs) getRPCSpan(ctx context.Context, ev core.Event) (*export.Span, *rpcStats) {
+func (r *Rpcs) getRPCSpan(ctx context.Context) (*export.Span, *rpcStats) {
// get the span
span := export.GetSpan(ctx)
if span == nil {
diff --git a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go
index d7ba381d3d5..62e416829fe 100644
--- a/gopls/internal/debug/serve.go
+++ b/gopls/internal/debug/serve.go
@@ -24,9 +24,9 @@ import (
"sync"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug/log"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/core"
@@ -51,7 +51,6 @@ type Instance struct {
Logfile string
StartTime time.Time
ServerAddress string
- Workdir string
OCAgentConfig string
LogWriter io.Writer
@@ -368,10 +367,9 @@ func GetInstance(ctx context.Context) *Instance {
// WithInstance creates debug instance ready for use using the supplied
// configuration and stores it in the returned context.
-func WithInstance(ctx context.Context, workdir, agent string) context.Context {
+func WithInstance(ctx context.Context, agent string) context.Context {
i := &Instance{
StartTime: time.Now(),
- Workdir: workdir,
OCAgentConfig: agent,
}
i.LogWriter = os.Stderr
@@ -464,7 +462,6 @@ func (i *Instance) Serve(ctx context.Context, addr string) (string, error) {
mux.HandleFunc("/analysis/", render(AnalysisTmpl, i.getAnalysis))
mux.HandleFunc("/cache/", render(CacheTmpl, i.getCache))
mux.HandleFunc("/session/", render(SessionTmpl, i.getSession))
- mux.HandleFunc("/view/", render(ViewTmpl, i.getView))
mux.HandleFunc("/client/", render(ClientTmpl, i.getClient))
mux.HandleFunc("/server/", render(ServerTmpl, i.getServer))
mux.HandleFunc("/file/", render(FileTmpl, i.getFile))
@@ -646,12 +643,17 @@ var BaseTemplate = template.Must(template.New("").Parse(`
width:6rem;
}
td.value {
- text-align: right;
+ text-align: right;
}
ul.spans {
font-family: monospace;
font-size: 85%;
}
+body {
+ font-family: sans-serif;
+ font-size: 1rem;
+ line-height: normal;
+}
{{block "head" .}}{{end}}
@@ -676,7 +678,6 @@ Unknown page
{{define "clientlink"}}Client {{.}}{{end}}
{{define "serverlink"}}Server {{.}}{{end}}
{{define "sessionlink"}}Session {{.}}{{end}}
-{{define "viewlink"}}View {{.}}{{end}}
`)).Funcs(template.FuncMap{
"fuint64": fuint64,
"fuint32": fuint32,
@@ -708,7 +709,7 @@ Unknown page
})
var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
-{{define "title"}}GoPls server information{{end}}
+{{define "title"}}Gopls server information{{end}}
{{define "body"}}
Caches
{{range .State.Caches}}- {{template "cachelink" .ID}}
{{end}}
@@ -724,7 +725,7 @@ var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
`))
var InfoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
-{{define "title"}}GoPls version information{{end}}
+{{define "title"}}Gopls version information{{end}}
{{define "body"}}
{{.}}
{{end}}
@@ -773,6 +774,13 @@ var CacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "body"}}
memoize.Store entries
{{range $k,$v := .MemStats}}- {{$k}} - {{$v}}
{{end}}
+File stats
+
+{{- $stats := .FileStats -}}
+Total: {{$stats.Total}}
+Largest: {{$stats.Largest}}
+Errors: {{$stats.Errs}}
+
{{end}}
`))
@@ -808,7 +816,16 @@ var SessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "body"}}
From: {{template "cachelink" .Cache.ID}}
Views
-{{range .Views}}- {{.Folder.Name}} is {{template "viewlink" .ID}} in {{.Folder.Dir}}
{{end}}
+{{range .Views}}
+{{- $envOverlay := .EnvOverlay -}}
+- ID: {{.ID}}
+Type: {{.Type}}
+Root: {{.Root}}
+{{- if $envOverlay}}
+Env overlay: {{$envOverlay}})
+{{end -}}
+Folder: {{.Folder.Name}}:{{.Folder.Dir}}
+{{end}}
Overlays
{{$session := .}}
{{range .Overlays}}
@@ -818,14 +835,6 @@ From: {{template "cachelink" .Cache.ID}}
{{end}}
`))
-var ViewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
-{{define "title"}}View {{.ID}}{{end}}
-{{define "body"}}
-Name: {{.Folder.Name}}
-Folder: {{.Folder.Dir}}
-{{end}}
-`))
-
var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Overlay {{.Identity.Hash}}{{end}}
{{define "body"}}
diff --git a/gopls/internal/debug/template_test.go b/gopls/internal/debug/template_test.go
index 53d4cb157b8..db940efc602 100644
--- a/gopls/internal/debug/template_test.go
+++ b/gopls/internal/debug/template_test.go
@@ -21,8 +21,9 @@ import (
"github.com/jba/templatecheck"
"golang.org/x/tools/go/packages"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug"
- "golang.org/x/tools/gopls/internal/lsp/cache"
+ "golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/internal/testenv"
)
@@ -30,16 +31,18 @@ var templates = map[string]struct {
tmpl *template.Template
data interface{} // a value of the needed type
}{
- "MainTmpl": {debug.MainTmpl, &debug.Instance{}},
- "DebugTmpl": {debug.DebugTmpl, nil},
- "RPCTmpl": {debug.RPCTmpl, &debug.Rpcs{}},
- "TraceTmpl": {debug.TraceTmpl, debug.TraceResults{}},
- "CacheTmpl": {debug.CacheTmpl, &cache.Cache{}},
- "SessionTmpl": {debug.SessionTmpl, &cache.Session{}},
- "ViewTmpl": {debug.ViewTmpl, &cache.View{}},
- "ClientTmpl": {debug.ClientTmpl, &debug.Client{}},
- "ServerTmpl": {debug.ServerTmpl, &debug.Server{}},
- "FileTmpl": {debug.FileTmpl, &cache.Overlay{}},
+ "MainTmpl": {debug.MainTmpl, &debug.Instance{}},
+ "DebugTmpl": {debug.DebugTmpl, nil},
+ "RPCTmpl": {debug.RPCTmpl, &debug.Rpcs{}},
+ "TraceTmpl": {debug.TraceTmpl, debug.TraceResults{}},
+ "CacheTmpl": {debug.CacheTmpl, &cache.Cache{}},
+ "SessionTmpl": {debug.SessionTmpl, &cache.Session{}},
+ "ClientTmpl": {debug.ClientTmpl, &debug.Client{}},
+ "ServerTmpl": {debug.ServerTmpl, &debug.Server{}},
+ "FileTmpl": {debug.FileTmpl, *new(interface {
+ file.Handle
+ Kind() file.Kind // (overlay files only)
+ })},
"InfoTmpl": {debug.InfoTmpl, "something"},
"MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}},
"AnalysisTmpl": {debug.AnalysisTmpl, new(debug.State).Analysis()},
@@ -75,7 +78,7 @@ func TestTemplates(t *testing.T) {
if tree == nil {
t.Fatalf("found no syntax tree for %s", "serve.go")
}
- renders := callsOf(p, tree, "render")
+ renders := callsOf(tree, "render")
if len(renders) == 0 {
t.Fatalf("found no calls to render")
}
@@ -123,7 +126,7 @@ func TestTemplates(t *testing.T) {
}
}
-func callsOf(p *packages.Package, tree *ast.File, name string) []*ast.CallExpr {
+func callsOf(tree *ast.File, name string) []*ast.CallExpr {
var ans []*ast.CallExpr
f := func(n ast.Node) bool {
x, ok := n.(*ast.CallExpr)
diff --git a/gopls/internal/debug/trace.go b/gopls/internal/debug/trace.go
index 31c5a5376ac..9314a04d241 100644
--- a/gopls/internal/debug/trace.go
+++ b/gopls/internal/debug/trace.go
@@ -150,14 +150,14 @@ func StdTrace(exporter event.Exporter) event.Exporter {
ctx = context.WithValue(ctx, traceKey, task)
}
// Log the start event as it may contain useful labels.
- msg := formatEvent(ctx, ev, lm)
+ msg := formatEvent(ev, lm)
trace.Log(ctx, "start", msg)
case event.IsLog(ev):
category := ""
if event.IsError(ev) {
category = "error"
}
- msg := formatEvent(ctx, ev, lm)
+ msg := formatEvent(ev, lm)
trace.Log(ctx, category, msg)
case event.IsEnd(ev):
if v := ctx.Value(traceKey); v != nil {
@@ -168,7 +168,7 @@ func StdTrace(exporter event.Exporter) event.Exporter {
}
}
-func formatEvent(ctx context.Context, ev core.Event, lm label.Map) string {
+func formatEvent(ev core.Event, lm label.Map) string {
buf := &bytes.Buffer{}
p := export.Printer{}
p.WriteEvent(buf, ev, lm)
diff --git a/gopls/internal/file/file.go b/gopls/internal/file/file.go
index 5a9d0aca5e2..5f8be06cf60 100644
--- a/gopls/internal/file/file.go
+++ b/gopls/internal/file/file.go
@@ -9,7 +9,7 @@ import (
"context"
"fmt"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// An Identity identifies the name and contents of a file.
diff --git a/gopls/internal/file/modification.go b/gopls/internal/file/modification.go
index 97313dda647..5deef718230 100644
--- a/gopls/internal/file/modification.go
+++ b/gopls/internal/file/modification.go
@@ -4,7 +4,7 @@
package file
-import "golang.org/x/tools/gopls/internal/lsp/protocol"
+import "golang.org/x/tools/gopls/internal/protocol"
// Modification represents a modification to a file.
type Modification struct {
diff --git a/gopls/internal/lsp/source/add_import.go b/gopls/internal/golang/add_import.go
similarity index 86%
rename from gopls/internal/lsp/source/add_import.go
rename to gopls/internal/golang/add_import.go
index 90d136d1904..201edce5306 100644
--- a/gopls/internal/lsp/source/add_import.go
+++ b/gopls/internal/golang/add_import.go
@@ -2,14 +2,14 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/imports"
)
diff --git a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go
similarity index 96%
rename from gopls/internal/lsp/source/call_hierarchy.go
rename to gopls/internal/golang/call_hierarchy.go
index e1b8f00f29f..87a6a54e458 100644
--- a/gopls/internal/lsp/source/call_hierarchy.go
+++ b/gopls/internal/golang/call_hierarchy.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -14,9 +14,9 @@ import (
"path/filepath"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/event"
@@ -25,7 +25,7 @@ import (
// PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file.
func PrepareCallHierarchy(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.CallHierarchyItem, error) {
- ctx, done := event.Start(ctx, "source.PrepareCallHierarchy")
+ ctx, done := event.Start(ctx, "golang.PrepareCallHierarchy")
defer done()
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
@@ -66,7 +66,7 @@ func PrepareCallHierarchy(ctx context.Context, snapshot *cache.Snapshot, fh file
// IncomingCalls returns an array of CallHierarchyIncomingCall for a file and the position within the file.
func IncomingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pos protocol.Position) ([]protocol.CallHierarchyIncomingCall, error) {
- ctx, done := event.Start(ctx, "source.IncomingCalls")
+ ctx, done := event.Start(ctx, "golang.IncomingCalls")
defer done()
refs, err := references(ctx, snapshot, fh, pos, false)
@@ -180,7 +180,7 @@ outer:
// OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file.
func OutgoingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) {
- ctx, done := event.Start(ctx, "source.OutgoingCalls")
+ ctx, done := event.Start(ctx, "golang.OutgoingCalls")
defer done()
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
diff --git a/gopls/internal/lsp/source/change_quote.go b/gopls/internal/golang/change_quote.go
similarity index 84%
rename from gopls/internal/lsp/source/change_quote.go
rename to gopls/internal/golang/change_quote.go
index 57dffdbc399..98a60e11ae0 100644
--- a/gopls/internal/lsp/source/change_quote.go
+++ b/gopls/internal/golang/change_quote.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"go/ast"
@@ -12,7 +12,7 @@ import (
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/diff"
@@ -78,17 +78,7 @@ func ConvertStringLiteral(pgf *ParsedGoFile, fh file.Handle, rng protocol.Range)
Title: title,
Kind: protocol.RefactorRewrite,
Edit: &protocol.WorkspaceEdit{
- DocumentChanges: []protocol.DocumentChanges{
- {
- TextDocumentEdit: &protocol.TextDocumentEdit{
- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
- Version: fh.Version(),
- TextDocumentIdentifier: protocol.TextDocumentIdentifier{URI: fh.URI()},
- },
- Edits: protocol.AsAnnotatedTextEdits(pedits),
- },
- },
- },
+ DocumentChanges: documentChanges(fh, pedits),
},
}, true
}
diff --git a/gopls/internal/lsp/source/change_signature.go b/gopls/internal/golang/change_signature.go
similarity index 97%
rename from gopls/internal/lsp/source/change_signature.go
rename to gopls/internal/golang/change_signature.go
index 76ea0d248de..e0a829e128e 100644
--- a/gopls/internal/lsp/source/change_signature.go
+++ b/gopls/internal/golang/change_signature.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"bytes"
@@ -16,10 +16,10 @@ import (
"regexp"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/imports"
@@ -134,6 +134,7 @@ func RemoveUnusedParameter(ctx context.Context, fh file.Handle, rng protocol.Ran
if err != nil {
return nil, err
}
+
// Finally, rewrite the original declaration. We do this after inlining all
// calls, as there may be calls in the same file as the declaration. But none
// of the inlining should have changed the location of the original
@@ -149,7 +150,10 @@ func RemoveUnusedParameter(ctx context.Context, fh file.Handle, rng protocol.Ran
src = pgf.Src
}
fset := tokeninternal.FileSetFor(pgf.Tok)
- src, err = rewriteSignature(fset, idx, src, newDecl)
+ src, err := rewriteSignature(fset, idx, src, newDecl)
+ if err != nil {
+ return nil, err
+ }
newContent[pgf.URI] = src
}
@@ -170,15 +174,7 @@ func RemoveUnusedParameter(ctx context.Context, fh file.Handle, rng protocol.Ran
if err != nil {
return nil, fmt.Errorf("computing edits for %s: %v", uri, err)
}
- changes = append(changes, protocol.DocumentChanges{
- TextDocumentEdit: &protocol.TextDocumentEdit{
- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
- Version: fh.Version(),
- TextDocumentIdentifier: protocol.TextDocumentIdentifier{URI: uri},
- },
- Edits: protocol.AsAnnotatedTextEdits(pedits),
- },
- })
+ changes = append(changes, documentChanges(fh, pedits)...)
}
return changes, nil
}
diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/golang/code_lens.go
similarity index 97%
rename from gopls/internal/lsp/source/code_lens.go
rename to gopls/internal/golang/code_lens.go
index 364665673d7..c4a7e5f82c8 100644
--- a/gopls/internal/lsp/source/code_lens.go
+++ b/gopls/internal/golang/code_lens.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -12,10 +12,10 @@ import (
"regexp"
"strings"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "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/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
)
type LensFunc func(context.Context, *cache.Snapshot, file.Handle) ([]protocol.CodeLens, error)
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
new file mode 100644
index 00000000000..5c6163cef12
--- /dev/null
+++ b/gopls/internal/golang/codeaction.go
@@ -0,0 +1,451 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package golang
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "go/ast"
+ "strings"
+
+ "golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/gopls/internal/analysis/fillstruct"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
+ "golang.org/x/tools/gopls/internal/settings"
+ "golang.org/x/tools/gopls/internal/util/bug"
+ "golang.org/x/tools/gopls/internal/util/slices"
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/event/tag"
+ "golang.org/x/tools/internal/imports"
+)
+
+// CodeActions returns all code actions (edits and other commands)
+// available for the selected range.
+func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) (actions []protocol.CodeAction, _ error) {
+ // Only compute quick fixes if there are any diagnostics to fix.
+ wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0
+
+ // Code actions requiring syntax information alone.
+ if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] {
+ pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull)
+ if err != nil {
+ return nil, err
+ }
+
+ // Process any missing imports and pair them with the diagnostics they fix.
+ if wantQuickFixes || want[protocol.SourceOrganizeImports] {
+ importEdits, importEditsPerFix, err := allImportsFixes(ctx, snapshot, pgf)
+ if err != nil {
+ event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Path()))
+ importEdits = nil
+ importEditsPerFix = nil
+ }
+
+ // Separate this into a set of codeActions per diagnostic, where
+ // each action is the addition, removal, or renaming of one import.
+ if wantQuickFixes {
+ for _, importFix := range importEditsPerFix {
+ fixed := fixedByImportFix(importFix.fix, diagnostics)
+ if len(fixed) == 0 {
+ continue
+ }
+ actions = append(actions, protocol.CodeAction{
+ Title: importFixTitle(importFix.fix),
+ Kind: protocol.QuickFix,
+ Edit: &protocol.WorkspaceEdit{
+ DocumentChanges: documentChanges(fh, importFix.edits),
+ },
+ Diagnostics: fixed,
+ })
+ }
+ }
+
+ // Send all of the import edits as one code action if the file is
+ // being organized.
+ if want[protocol.SourceOrganizeImports] && len(importEdits) > 0 {
+ actions = append(actions, protocol.CodeAction{
+ Title: "Organize Imports",
+ Kind: protocol.SourceOrganizeImports,
+ Edit: &protocol.WorkspaceEdit{
+ DocumentChanges: documentChanges(fh, importEdits),
+ },
+ })
+ }
+ }
+
+ if want[protocol.RefactorExtract] {
+ extractions, err := getExtractCodeActions(pgf, rng, snapshot.Options())
+ if err != nil {
+ return nil, err
+ }
+ actions = append(actions, extractions...)
+ }
+ }
+
+ // Code actions requiring type information.
+ if want[protocol.RefactorRewrite] ||
+ want[protocol.RefactorInline] ||
+ want[protocol.GoTest] {
+ pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+ if want[protocol.RefactorRewrite] {
+ rewrites, err := getRewriteCodeActions(pkg, pgf, fh, rng, snapshot.Options())
+ if err != nil {
+ return nil, err
+ }
+ actions = append(actions, rewrites...)
+ }
+
+ if want[protocol.RefactorInline] {
+ rewrites, err := getInlineCodeActions(pkg, pgf, rng, snapshot.Options())
+ if err != nil {
+ return nil, err
+ }
+ actions = append(actions, rewrites...)
+ }
+
+ if want[protocol.GoTest] {
+ fixes, err := getGoTestCodeActions(pkg, pgf, rng)
+ if err != nil {
+ return nil, err
+ }
+ actions = append(actions, fixes...)
+ }
+ }
+ return actions, nil
+}
+
+func supportsResolveEdits(options *settings.Options) bool {
+ return options.CodeActionResolveOptions != nil && slices.Contains(options.CodeActionResolveOptions, "edit")
+}
+
+func importFixTitle(fix *imports.ImportFix) string {
+ var str string
+ switch fix.FixType {
+ case imports.AddImport:
+ str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
+ case imports.DeleteImport:
+ str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
+ case imports.SetImportName:
+ str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
+ }
+ return str
+}
+
+// fixedByImportFix filters the provided slice of diagnostics to those that
+// would be fixed by the provided imports fix.
+func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) []protocol.Diagnostic {
+ var results []protocol.Diagnostic
+ for _, diagnostic := range diagnostics {
+ switch {
+ // "undeclared name: X" may be an unresolved import.
+ case strings.HasPrefix(diagnostic.Message, "undeclared name: "):
+ ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ")
+ 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: ")
+ if ident == fix.IdentName {
+ results = append(results, diagnostic)
+ }
+ // "X imported but not used" is an unused import.
+ // "X imported but not used as Y" is an unused import.
+ case strings.Contains(diagnostic.Message, " imported but not used"):
+ idx := strings.Index(diagnostic.Message, " imported but not used")
+ importPath := diagnostic.Message[:idx]
+ if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) {
+ results = append(results, diagnostic)
+ }
+ }
+ }
+ return results
+}
+
+// getExtractCodeActions returns any refactor.extract code actions for the selection.
+func getExtractCodeActions(pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) {
+ if rng.Start == rng.End {
+ return nil, nil
+ }
+
+ start, end, err := pgf.RangePos(rng)
+ if err != nil {
+ return nil, err
+ }
+ puri := pgf.URI
+ var commands []protocol.Command
+ if _, ok, methodOk, _ := CanExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok {
+ cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{
+ Fix: fixExtractFunction,
+ URI: puri,
+ Range: rng,
+ ResolveEdits: supportsResolveEdits(options),
+ })
+ if err != nil {
+ return nil, err
+ }
+ commands = append(commands, cmd)
+ if methodOk {
+ cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{
+ Fix: fixExtractMethod,
+ URI: puri,
+ Range: rng,
+ ResolveEdits: supportsResolveEdits(options),
+ })
+ if err != nil {
+ return nil, err
+ }
+ commands = append(commands, cmd)
+ }
+ }
+ if _, _, ok, _ := CanExtractVariable(start, end, pgf.File); ok {
+ cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{
+ Fix: fixExtractVariable,
+ URI: puri,
+ Range: rng,
+ ResolveEdits: supportsResolveEdits(options),
+ })
+ if err != nil {
+ return nil, err
+ }
+ commands = append(commands, cmd)
+ }
+ var actions []protocol.CodeAction
+ for i := range commands {
+ actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorExtract, &commands[i], nil, options))
+ }
+ return actions, nil
+}
+
+func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Command, diagnostics []protocol.Diagnostic, options *settings.Options) protocol.CodeAction {
+ action := protocol.CodeAction{
+ Title: title,
+ Kind: kind,
+ Diagnostics: diagnostics,
+ }
+ if !supportsResolveEdits(options) {
+ action.Command = cmd
+ } else {
+ data, err := json.Marshal(cmd)
+ if err != nil {
+ panic("unable to marshal")
+ }
+ msg := json.RawMessage(data)
+ action.Data = &msg
+ }
+ return action
+}
+
+// getRewriteCodeActions returns refactor.rewrite code actions available at the specified range.
+func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) {
+ // golang/go#61693: code actions were refactored to run outside of the
+ // analysis framework, but as a result they lost their panic recovery.
+ //
+ // These code actions should never fail, but put back the panic recovery as a
+ // defensive measure.
+ defer func() {
+ if r := recover(); r != nil {
+ rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r)
+ }
+ }()
+
+ var actions []protocol.CodeAction
+
+ if canRemoveParameter(pkg, pgf, rng) {
+ cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{
+ RemoveParameter: protocol.Location{
+ URI: pgf.URI,
+ Range: rng,
+ },
+ ResolveEdits: supportsResolveEdits(options),
+ })
+ if err != nil {
+ return nil, err
+ }
+ actions = append(actions, newCodeAction("Refactor: remove unused parameter", protocol.RefactorRewrite, &cmd, nil, options))
+ }
+
+ if action, ok := ConvertStringLiteral(pgf, fh, rng); ok {
+ actions = append(actions, action)
+ }
+
+ start, end, err := pgf.RangePos(rng)
+ if err != nil {
+ return nil, err
+ }
+
+ var commands []protocol.Command
+ if _, ok, _ := CanInvertIfCondition(pgf.File, start, end); ok {
+ cmd, err := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{
+ Fix: fixInvertIfCondition,
+ URI: pgf.URI,
+ Range: rng,
+ ResolveEdits: supportsResolveEdits(options),
+ })
+ if err != nil {
+ return nil, err
+ }
+ commands = append(commands, cmd)
+ }
+
+ // N.B.: an inspector only pays for itself after ~5 passes, which means we're
+ // currently not getting a good deal on this inspection.
+ //
+ // TODO: Consider removing the inspection after convenienceAnalyzers are removed.
+ inspect := inspector.New([]*ast.File{pgf.File})
+ for _, diag := range fillstruct.Diagnose(inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) {
+ rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End)
+ if err != nil {
+ return nil, err
+ }
+ for _, fix := range diag.SuggestedFixes {
+ cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{
+ Fix: diag.Category,
+ URI: pgf.URI,
+ Range: rng,
+ ResolveEdits: supportsResolveEdits(options),
+ })
+ if err != nil {
+ return nil, err
+ }
+ commands = append(commands, cmd)
+ }
+ }
+
+ for i := range commands {
+ actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorRewrite, &commands[i], nil, options))
+ }
+
+ return actions, nil
+}
+
+// canRemoveParameter reports whether we can remove the function parameter
+// indicated by the given [start, end) range.
+//
+// This is true if:
+// - [start, end) is contained within an unused field or parameter name
+// - ... of a non-method function declaration.
+//
+// (Note that the unusedparam analyzer also computes this property, but
+// much more precisely, allowing it to report its findings as diagnostics.)
+func canRemoveParameter(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) bool {
+ info, err := FindParam(pgf, rng)
+ if err != nil {
+ return false // e.g. invalid range
+ }
+ if info.Field == nil {
+ return false // range does not span a parameter
+ }
+ if info.Decl.Body == nil {
+ return false // external function
+ }
+ if len(info.Field.Names) == 0 {
+ return true // no names => field is unused
+ }
+ if info.Name == nil {
+ return false // no name is indicated
+ }
+ if info.Name.Name == "_" {
+ return true // trivially unused
+ }
+
+ obj := pkg.GetTypesInfo().Defs[info.Name]
+ if obj == nil {
+ return false // something went wrong
+ }
+
+ used := false
+ ast.Inspect(info.Decl.Body, func(node ast.Node) bool {
+ if n, ok := node.(*ast.Ident); ok && pkg.GetTypesInfo().Uses[n] == obj {
+ used = true
+ }
+ return !used // keep going until we find a use
+ })
+ return !used
+}
+
+// getInlineCodeActions returns refactor.inline actions available at the specified range.
+func getInlineCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) {
+ start, end, err := pgf.RangePos(rng)
+ if err != nil {
+ return nil, err
+ }
+
+ // If range is within call expression, offer inline action.
+ var commands []protocol.Command
+ if _, fn, err := EnclosingStaticCall(pkg, pgf, start, end); err == nil {
+ cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{
+ Fix: fixInlineCall,
+ URI: pgf.URI,
+ Range: rng,
+ ResolveEdits: supportsResolveEdits(options),
+ })
+ if err != nil {
+ return nil, err
+ }
+ commands = append(commands, cmd)
+ }
+
+ // Convert commands to actions.
+ var actions []protocol.CodeAction
+ for i := range commands {
+ actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorInline, &commands[i], nil, options))
+ }
+ return actions, nil
+}
+
+// getGoTestCodeActions returns any "run this test/benchmark" code actions for the selection.
+func getGoTestCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) {
+ fns, err := TestsAndBenchmarks(pkg, pgf)
+ if err != nil {
+ return nil, err
+ }
+
+ var tests, benchmarks []string
+ for _, fn := range fns.Tests {
+ if !protocol.Intersect(fn.Rng, rng) {
+ continue
+ }
+ tests = append(tests, fn.Name)
+ }
+ for _, fn := range fns.Benchmarks {
+ if !protocol.Intersect(fn.Rng, rng) {
+ continue
+ }
+ benchmarks = append(benchmarks, fn.Name)
+ }
+
+ if len(tests) == 0 && len(benchmarks) == 0 {
+ return nil, nil
+ }
+
+ cmd, err := command.NewTestCommand("Run tests and benchmarks", pgf.URI, tests, benchmarks)
+ if err != nil {
+ return nil, err
+ }
+ return []protocol.CodeAction{{
+ Title: cmd.Title,
+ Kind: protocol.GoTest,
+ Command: &cmd,
+ }}, nil
+}
+
+func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges {
+ return protocol.TextEditsToDocumentChanges(fh.URI(), fh.Version(), edits)
+}
diff --git a/gopls/internal/lsp/source/comment.go b/gopls/internal/golang/comment.go
similarity index 99%
rename from gopls/internal/lsp/source/comment.go
rename to gopls/internal/golang/comment.go
index 578399e30c6..ece123801ff 100644
--- a/gopls/internal/lsp/source/comment.go
+++ b/gopls/internal/golang/comment.go
@@ -5,7 +5,7 @@
//go:build !go1.19
// +build !go1.19
-package source
+package golang
import (
"bytes"
diff --git a/gopls/internal/lsp/source/comment_go118_test.go b/gopls/internal/golang/comment_go118_test.go
similarity index 99%
rename from gopls/internal/lsp/source/comment_go118_test.go
rename to gopls/internal/golang/comment_go118_test.go
index 60bd14b9fc8..c38898a28e6 100644
--- a/gopls/internal/lsp/source/comment_go118_test.go
+++ b/gopls/internal/golang/comment_go118_test.go
@@ -5,7 +5,7 @@
//go:build !go1.19
// +build !go1.19
-package source
+package golang
import (
"bytes"
diff --git a/gopls/internal/lsp/source/comment_go119.go b/gopls/internal/golang/comment_go119.go
similarity index 82%
rename from gopls/internal/lsp/source/comment_go119.go
rename to gopls/internal/golang/comment_go119.go
index d36d110b541..eec338d54fa 100644
--- a/gopls/internal/lsp/source/comment_go119.go
+++ b/gopls/internal/golang/comment_go119.go
@@ -5,17 +5,13 @@
//go:build go1.19
// +build go1.19
-package source
+package golang
// 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.)
+// slightly different results.
// 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.
diff --git a/gopls/internal/lsp/source/completion/builtin.go b/gopls/internal/golang/completion/builtin.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/builtin.go
rename to gopls/internal/golang/completion/builtin.go
diff --git a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/golang/completion/completion.go
similarity index 97%
rename from gopls/internal/lsp/source/completion/completion.go
rename to gopls/internal/golang/completion/completion.go
index d241cbdf31a..b796de4932d 100644
--- a/gopls/internal/lsp/source/completion/completion.go
+++ b/gopls/internal/golang/completion/completion.go
@@ -27,15 +27,16 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "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/snippet"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/golang/completion/snippet"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
goplsastutil "golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/fuzzy"
"golang.org/x/tools/internal/imports"
@@ -172,7 +173,7 @@ type completer struct {
snapshot *cache.Snapshot
pkg *cache.Package
qf types.Qualifier // for qualifying typed expressions
- mq source.MetadataQualifier // for syntactic qualifying
+ mq golang.MetadataQualifier // for syntactic qualifying
opts *completionOptions
// completionContext contains information about the trigger for this
@@ -376,6 +377,7 @@ func (c *completer) getSurrounding() *Selection {
type candidate struct {
// obj is the types.Object to complete to.
// TODO(adonovan): eliminate dependence on go/types throughout this struct.
+ // See comment in (*completer).selector for explanation.
obj types.Object
// score is used to rank candidates.
@@ -451,7 +453,7 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
startTime := time.Now()
- pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI())
+ pkg, pgf, err := golang.NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil || pgf.File.Package == token.NoPos {
// If we can't parse this file or find position for the package
// keyword, it may be missing a package declaration. Try offering
@@ -520,15 +522,15 @@ func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
}
// Collect all surrounding scopes, innermost first.
- scopes := source.CollectScopes(pkg.GetTypesInfo(), path, pos)
+ scopes := golang.CollectScopes(pkg.GetTypesInfo(), path, pos)
scopes = append(scopes, pkg.GetTypes().Scope(), types.Universe)
opts := snapshot.Options()
c := &completer{
pkg: pkg,
snapshot: snapshot,
- qf: source.Qualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()),
- mq: source.MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()),
+ qf: typesutil.FileQualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()),
+ mq: golang.MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()),
completionContext: completionContext{
triggerCharacter: protoContext.TriggerCharacter,
triggerKind: protoContext.TriggerKind,
@@ -645,7 +647,7 @@ func (c *completer) collectCompletions(ctx context.Context) error {
// Inside comments, offer completions for the name of the relevant symbol.
for _, comment := range c.file.Comments {
if comment.Pos() < c.pos && c.pos <= comment.End() {
- c.populateCommentCompletions(ctx, comment)
+ c.populateCommentCompletions(comment)
return nil
}
}
@@ -920,7 +922,7 @@ func (c *completer) populateImportCompletions(searchImport *ast.ImportSpec) erro
}
// populateCommentCompletions yields completions for comments preceding or in declarations.
-func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast.CommentGroup) {
+func (c *completer) populateCommentCompletions(comment *ast.CommentGroup) {
// If the completion was triggered by a period, ignore it. These types of
// completions will not be useful in comments.
if c.completionContext.triggerCharacter == "." {
@@ -972,12 +974,12 @@ func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast
// add TypeSpec fields to completion
switch typeNode := spec.Type.(type) {
case *ast.StructType:
- c.addFieldItems(ctx, typeNode.Fields)
+ c.addFieldItems(typeNode.Fields)
case *ast.FuncType:
- c.addFieldItems(ctx, typeNode.Params)
- c.addFieldItems(ctx, typeNode.Results)
+ c.addFieldItems(typeNode.Params)
+ c.addFieldItems(typeNode.Results)
case *ast.InterfaceType:
- c.addFieldItems(ctx, typeNode.Methods)
+ c.addFieldItems(typeNode.Methods)
}
if spec.Name.String() == "_" {
@@ -998,9 +1000,9 @@ func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast
}
// handle functions
case *ast.FuncDecl:
- c.addFieldItems(ctx, node.Recv)
- c.addFieldItems(ctx, node.Type.Params)
- c.addFieldItems(ctx, node.Type.Results)
+ c.addFieldItems(node.Recv)
+ c.addFieldItems(node.Type.Params)
+ c.addFieldItems(node.Type.Results)
// collect receiver struct fields
if node.Recv != nil {
@@ -1084,7 +1086,7 @@ func isValidIdentifierChar(char byte) bool {
}
// adds struct fields, interface methods, function declaration fields to completion
-func (c *completer) addFieldItems(ctx context.Context, fields *ast.FieldList) {
+func (c *completer) addFieldItems(fields *ast.FieldList) {
if fields == nil {
return
}
@@ -1156,7 +1158,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
imp := pkgName.Imported()
// Known direct dependency? Expand using type information.
- if _, ok := c.pkg.Metadata().DepsByPkgPath[source.PackagePath(imp.Path())]; ok {
+ if _, ok := c.pkg.Metadata().DepsByPkgPath[golang.PackagePath(imp.Path())]; ok {
c.packageMembers(imp, stdScore, nil, c.deepState.enqueue)
return nil
}
@@ -1208,7 +1210,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
if err != nil {
return err
}
- known := make(map[source.PackagePath]*metadata.Package)
+ known := make(map[golang.PackagePath]*metadata.Package)
for _, mp := range all {
if mp.Name == "main" {
continue // not importable
@@ -1346,6 +1348,11 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
return params
}
+ // Ideally we would eliminate the suffix of type
+ // parameters that are redundant with inference
+ // from the argument types (#51783), but it's
+ // quite fiddly to do using syntax alone.
+ // (See inferableTypeParams in format.go.)
tparams := paramList(fn.Type.TypeParams)
params := paramList(fn.Type.Params)
var sn snippet.Builder
@@ -1366,7 +1373,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
// Extract the package-level candidates using a quick parse.
var g errgroup.Group
for _, path := range paths {
- mp := known[source.PackagePath(path)]
+ mp := known[golang.PackagePath(path)]
for _, uri := range mp.CompiledGoFiles {
uri := uri
g.Go(func() error {
@@ -1389,7 +1396,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
mu.Lock()
defer mu.Unlock()
// TODO(adonovan): what if the actual package has a vendor/ prefix?
- if _, ok := known[source.PackagePath(pkgExport.Fix.StmtInfo.ImportPath)]; ok {
+ if _, ok := known[golang.PackagePath(pkgExport.Fix.StmtInfo.ImportPath)]; ok {
return // We got this one above.
}
@@ -1459,7 +1466,7 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *impo
if isStarTestingDotF(typ) && addressable {
// is that a sufficient test? (or is more care needed?)
- if c.fuzz(typ, mset, imp, cb, c.pkg.FileSet()) {
+ if c.fuzz(mset, imp, cb) {
return
}
}
@@ -1578,7 +1585,7 @@ func (c *completer) lexical(ctx context.Context) error {
}
if c.inference.objType != nil {
- if named, _ := source.Deref(c.inference.objType).(*types.Named); named != nil {
+ if named, _ := golang.Deref(c.inference.objType).(*types.Named); named != nil {
// If we expected a named type, check the type's package for
// completion items. This is useful when the current file hasn't
// imported the type's package yet.
@@ -1589,7 +1596,7 @@ func (c *completer) lexical(ctx context.Context) error {
// Make sure the package name isn't already in use by another
// object, and that this file doesn't import the package yet.
// TODO(adonovan): what if pkg.Path has vendor/ prefix?
- if _, ok := seen[pkg.Name()]; !ok && pkg != c.pkg.GetTypes() && !alreadyImports(c.file, source.ImportPath(pkg.Path())) {
+ if _, ok := seen[pkg.Name()]; !ok && pkg != c.pkg.GetTypes() && !alreadyImports(c.file, golang.ImportPath(pkg.Path())) {
seen[pkg.Name()] = struct{}{}
obj := types.NewPkgName(0, nil, pkg.Name(), pkg)
imp := &importInfo{
@@ -1644,7 +1651,7 @@ func (c *completer) injectType(ctx context.Context, t types.Type) {
return
}
- t = source.Deref(t)
+ t = golang.Deref(t)
// If we have an expected type and it is _not_ a named type, handle
// it specially. Non-named types like "[]int" will never be
@@ -1692,7 +1699,7 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru
if err != nil {
return err
}
- pkgNameByPath := make(map[source.PackagePath]string)
+ pkgNameByPath := make(map[golang.PackagePath]string)
var paths []string // actually PackagePaths
for _, mp := range all {
if mp.ForTest != "" {
@@ -1730,7 +1737,7 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru
})
for _, path := range paths {
- name := pkgNameByPath[source.PackagePath(path)]
+ name := pkgNameByPath[golang.PackagePath(path)]
if _, ok := seen[name]; ok {
continue
}
@@ -1752,8 +1759,6 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru
count++
}
- ctx, cancel := context.WithCancel(ctx)
-
var mu sync.Mutex
add := func(pkg imports.ImportFix) {
if ignoreUnimportedCompletion(&pkg) {
@@ -1769,7 +1774,6 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru
}
if count >= maxUnimportedPackageNames {
- cancel()
return
}
@@ -1787,15 +1791,16 @@ func (c *completer) unimportedPackages(ctx context.Context, seen map[string]stru
})
count++
}
+
c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error {
- defer cancel()
return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.GetTypes().Name(), opts.Env)
})
+
return nil
}
// alreadyImports reports whether f has an import with the specified path.
-func alreadyImports(f *ast.File, path source.ImportPath) bool {
+func alreadyImports(f *ast.File, path golang.ImportPath) bool {
for _, s := range f.Imports {
if metadata.UnquoteImportPath(s) == path {
return true
@@ -1891,7 +1896,7 @@ func enclosingCompositeLiteral(path []ast.Node, pos token.Pos, info *types.Info)
clInfo := compLitInfo{
cl: n,
- clType: source.Deref(tv.Type).Underlying(),
+ clType: golang.Deref(tv.Type).Underlying(),
}
var (
diff --git a/gopls/internal/lsp/source/completion/deep_completion.go b/gopls/internal/golang/completion/deep_completion.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/deep_completion.go
rename to gopls/internal/golang/completion/deep_completion.go
diff --git a/gopls/internal/lsp/source/completion/deep_completion_test.go b/gopls/internal/golang/completion/deep_completion_test.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/deep_completion_test.go
rename to gopls/internal/golang/completion/deep_completion_test.go
diff --git a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/golang/completion/definition.go
similarity index 94%
rename from gopls/internal/lsp/source/completion/definition.go
rename to gopls/internal/golang/completion/definition.go
index e4c184186aa..1e3852bffdb 100644
--- a/gopls/internal/lsp/source/completion/definition.go
+++ b/gopls/internal/golang/completion/definition.go
@@ -11,9 +11,9 @@ import (
"unicode"
"unicode/utf8"
- "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/snippet"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/golang/completion/snippet"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// some function definitions in test files can be completed
@@ -21,7 +21,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, pgf *source.ParsedGoFile) ([]CompletionItem, *Selection) {
+func definition(path []ast.Node, obj types.Object, pgf *golang.ParsedGoFile) ([]CompletionItem, *Selection) {
if _, ok := obj.(*types.Func); !ok {
return nil, nil // not a function at all
}
diff --git a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/golang/completion/format.go
similarity index 74%
rename from gopls/internal/lsp/source/completion/format.go
rename to gopls/internal/golang/completion/format.go
index 0d47161c4d0..e533860a6a3 100644
--- a/gopls/internal/lsp/source/completion/format.go
+++ b/gopls/internal/golang/completion/format.go
@@ -13,9 +13,9 @@ import (
"go/types"
"strings"
- "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/snippet"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/golang/completion/snippet"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
@@ -62,7 +62,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e
x := cand.obj.(*types.TypeName)
if named, ok := x.Type().(*types.Named); ok {
tp := named.TypeParams()
- label += source.FormatTypeParams(tp)
+ label += golang.FormatTypeParams(tp)
insert = label // maintain invariant above (label == insert)
}
}
@@ -71,7 +71,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e
switch obj := obj.(type) {
case *types.TypeName:
- detail, kind = source.FormatType(obj.Type(), c.qf)
+ detail, kind = golang.FormatType(obj.Type(), c.qf)
case *types.Const:
kind = protocol.ConstantCompletion
case *types.Var:
@@ -79,7 +79,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e
detail = "struct{...}" // for anonymous structs
} else if obj.IsField() {
var err error
- detail, err = source.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf, c.mq)
+ detail, err = golang.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf, c.mq)
if err != nil {
return CompletionItem{}, err
}
@@ -131,11 +131,31 @@ Suffixes:
switch mod {
case invoke:
if sig, ok := funcType.Underlying().(*types.Signature); ok {
- s, err := source.NewSignature(ctx, c.snapshot, c.pkg, sig, nil, c.qf, c.mq)
+ s, err := golang.NewSignature(ctx, c.snapshot, c.pkg, sig, nil, c.qf, c.mq)
if err != nil {
return CompletionItem{}, err
}
- c.functionCallSnippet("", s.TypeParams(), s.Params(), &snip)
+
+ tparams := s.TypeParams()
+ if len(tparams) > 0 {
+ // Eliminate the suffix of type parameters that are
+ // likely redundant because they can probably be
+ // inferred from the argument types (#51783).
+ //
+ // We don't bother doing the reverse inference from
+ // result types as result-only type parameters are
+ // quite unusual.
+ free := inferableTypeParams(sig)
+ for i := sig.TypeParams().Len() - 1; i >= 0; i-- {
+ tparam := sig.TypeParams().At(i)
+ if !free[tparam] {
+ break
+ }
+ tparams = tparams[:i] // eliminate
+ }
+ }
+
+ c.functionCallSnippet("", tparams, s.Params(), &snip)
if sig.Results().Len() == 1 {
funcType = sig.Results().At(0).Type()
}
@@ -247,7 +267,7 @@ Suffixes:
return item, nil
}
- comment, err := source.HoverDocForObject(ctx, c.snapshot, c.pkg.FileSet(), obj)
+ comment, err := golang.HoverDocForObject(ctx, c.snapshot, c.pkg.FileSet(), obj)
if err != nil {
event.Error(ctx, fmt.Sprintf("failed to find Hover for %q", obj.Name()), err)
return item, nil
@@ -282,7 +302,7 @@ func (c *completer) importEdits(imp *importInfo) ([]protocol.TextEdit, error) {
return nil, err
}
- return source.ComputeOneImportFixEdits(c.snapshot, pgf, &imports.ImportFix{
+ return golang.ComputeOneImportFixEdits(c.snapshot, pgf, &imports.ImportFix{
StmtInfo: imports.ImportInfo{
ImportPath: imp.importPath,
Name: imp.name,
@@ -304,12 +324,13 @@ func (c *completer) formatBuiltin(ctx context.Context, cand candidate) (Completi
item.Kind = protocol.ConstantCompletion
case *types.Builtin:
item.Kind = protocol.FunctionCompletion
- sig, err := source.NewBuiltinSignature(ctx, c.snapshot, obj.Name())
+ sig, err := golang.NewBuiltinSignature(ctx, c.snapshot, obj.Name())
if err != nil {
return CompletionItem{}, err
}
item.Detail = "func" + sig.Format()
item.snippet = &snippet.Builder{}
+ // The signature inferred for a built-in is instantiated, so TypeParams=∅.
c.functionCallSnippet(obj.Name(), sig.TypeParams(), sig.Params(), item.snippet)
case *types.TypeName:
if types.IsInterface(obj.Type()) {
@@ -341,3 +362,78 @@ func (c *completer) wantTypeParams() bool {
}
return false
}
+
+// inferableTypeParams returns the set of type parameters
+// of sig that are constrained by (inferred from) the argument types.
+func inferableTypeParams(sig *types.Signature) map[*types.TypeParam]bool {
+ free := make(map[*types.TypeParam]bool)
+
+ // visit adds to free all the free type parameters of t.
+ var visit func(t types.Type)
+ visit = func(t types.Type) {
+ switch t := t.(type) {
+ case *types.Array:
+ visit(t.Elem())
+ case *types.Chan:
+ visit(t.Elem())
+ case *types.Map:
+ visit(t.Key())
+ visit(t.Elem())
+ case *types.Pointer:
+ visit(t.Elem())
+ case *types.Slice:
+ visit(t.Elem())
+ case *types.Interface:
+ for i := 0; i < t.NumExplicitMethods(); i++ {
+ visit(t.ExplicitMethod(i).Type())
+ }
+ for i := 0; i < t.NumEmbeddeds(); i++ {
+ visit(t.EmbeddedType(i))
+ }
+ case *types.Union:
+ for i := 0; i < t.Len(); i++ {
+ visit(t.Term(i).Type())
+ }
+ case *types.Signature:
+ if tp := t.TypeParams(); tp != nil {
+ // Generic signatures only appear as the type of generic
+ // function declarations, so this isn't really reachable.
+ for i := 0; i < tp.Len(); i++ {
+ visit(tp.At(i).Constraint())
+ }
+ }
+ visit(t.Params())
+ visit(t.Results())
+ case *types.Tuple:
+ for i := 0; i < t.Len(); i++ {
+ visit(t.At(i).Type())
+ }
+ case *types.Struct:
+ for i := 0; i < t.NumFields(); i++ {
+ visit(t.Field(i).Type())
+ }
+ case *types.TypeParam:
+ free[t] = true
+ case *types.Basic, *types.Named:
+ // nop
+ default:
+ panic(t)
+ }
+ }
+
+ visit(sig.Params())
+
+ // Perform induction through constraints.
+restart:
+ for i := 0; i < sig.TypeParams().Len(); i++ {
+ tp := sig.TypeParams().At(i)
+ if free[tp] {
+ n := len(free)
+ visit(tp.Constraint())
+ if len(free) > n {
+ goto restart // iterate until fixed point
+ }
+ }
+ }
+ return free
+}
diff --git a/gopls/internal/lsp/source/completion/fuzz.go b/gopls/internal/golang/completion/fuzz.go
similarity index 94%
rename from gopls/internal/lsp/source/completion/fuzz.go
rename to gopls/internal/golang/completion/fuzz.go
index 08e7654c7ed..382676afc02 100644
--- a/gopls/internal/lsp/source/completion/fuzz.go
+++ b/gopls/internal/golang/completion/fuzz.go
@@ -7,11 +7,10 @@ package completion
import (
"fmt"
"go/ast"
- "go/token"
"go/types"
"strings"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// golang/go#51089
@@ -21,7 +20,7 @@ import (
// PJW: are there other packages where we can deduce usage constraints?
// if we find fuzz completions, then return true, as those are the only completions to offer
-func (c *completer) fuzz(typ types.Type, mset *types.MethodSet, imp *importInfo, cb func(candidate), fset *token.FileSet) bool {
+func (c *completer) fuzz(mset *types.MethodSet, imp *importInfo, cb func(candidate)) bool {
// 1. inside f.Fuzz? (only f.Failed and f.Name)
// 2. possible completing f.Fuzz?
// [Ident,SelectorExpr,Callexpr,ExprStmt,BlockiStmt,FuncDecl(Fuzz...)]
diff --git a/gopls/internal/lsp/source/completion/keywords.go b/gopls/internal/golang/completion/keywords.go
similarity index 98%
rename from gopls/internal/lsp/source/completion/keywords.go
rename to gopls/internal/golang/completion/keywords.go
index 1dcd58411c5..cad04fd7a6d 100644
--- a/gopls/internal/lsp/source/completion/keywords.go
+++ b/gopls/internal/golang/completion/keywords.go
@@ -7,7 +7,7 @@ package completion
import (
"go/ast"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/astutil"
)
diff --git a/gopls/internal/lsp/source/completion/labels.go b/gopls/internal/golang/completion/labels.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/labels.go
rename to gopls/internal/golang/completion/labels.go
diff --git a/gopls/internal/lsp/source/completion/literal.go b/gopls/internal/golang/completion/literal.go
similarity index 97%
rename from gopls/internal/lsp/source/completion/literal.go
rename to gopls/internal/golang/completion/literal.go
index b48407b12e1..45f772d789f 100644
--- a/gopls/internal/lsp/source/completion/literal.go
+++ b/gopls/internal/golang/completion/literal.go
@@ -11,9 +11,9 @@ import (
"strings"
"unicode"
- "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/snippet"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/golang/completion/snippet"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
@@ -50,7 +50,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im
// don't offer "mySlice{}" since we have already added a candidate
// of "[]int{}".
if _, named := literalType.(*types.Named); named && expType != nil {
- if _, named := source.Deref(expType).(*types.Named); !named {
+ if _, named := golang.Deref(expType).(*types.Named); !named {
return
}
}
@@ -201,9 +201,9 @@ 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".
- typeName, err := source.FormatVarType(ctx, c.snapshot, c.pkg, p,
+ typeName, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, p,
func(p *types.Package) string { return "" },
- func(source.PackageName, source.ImportPath, source.PackagePath) string { return "" })
+ func(golang.PackageName, golang.ImportPath, golang.PackagePath) string { return "" })
if err != nil {
// In general, the only error we should encounter while formatting is
// context cancellation.
@@ -271,7 +271,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m
// of "i int, j int".
if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) {
snip.WriteText(" ")
- typeStr, err := source.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf, c.mq)
+ typeStr, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf, c.mq)
if err != nil {
// In general, the only error we should encounter while formatting is
// context cancellation.
@@ -329,7 +329,7 @@ func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, m
snip.WriteText(name + " ")
}
- text, err := source.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf, c.mq)
+ text, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf, c.mq)
if err != nil {
// In general, the only error we should encounter while formatting is
// context cancellation.
diff --git a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/golang/completion/package.go
similarity index 96%
rename from gopls/internal/lsp/source/completion/package.go
rename to gopls/internal/golang/completion/package.go
index 084191f90a4..709d65ce3a7 100644
--- a/gopls/internal/lsp/source/completion/package.go
+++ b/gopls/internal/golang/completion/package.go
@@ -18,10 +18,10 @@ import (
"strings"
"unicode"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "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/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/fuzzy"
)
@@ -32,7 +32,7 @@ func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh
// We know that the AST for this file will be empty due to the missing
// package declaration, but parse it anyway to get a mapper.
// TODO(adonovan): opt: there's no need to parse just to get a mapper.
- pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull)
+ pgf, err := snapshot.ParseGo(ctx, fh, golang.ParseFull)
if err != nil {
return nil, nil, err
}
@@ -68,7 +68,7 @@ func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh
// packageCompletionSurrounding returns surrounding for package completion if a
// package completions can be suggested at a given cursor offset. A valid location
// for package completion is above any declarations or import statements.
-func packageCompletionSurrounding(pgf *source.ParsedGoFile, offset int) (*Selection, error) {
+func packageCompletionSurrounding(pgf *golang.ParsedGoFile, offset int) (*Selection, error) {
m := pgf.Mapper
// If the file lacks a package declaration, the parser will return an empty
// AST. As a work-around, try to parse an expression from the file contents.
@@ -230,7 +230,7 @@ func packageSuggestions(ctx context.Context, snapshot *cache.Snapshot, fileURI p
}
pkgName := convertDirNameToPkgName(dirName)
- seenPkgs := make(map[source.PackageName]struct{})
+ seenPkgs := make(map[golang.PackageName]struct{})
// The `go` command by default only allows one package per directory but we
// support multiple package suggestions since gopls is build system agnostic.
@@ -320,7 +320,7 @@ func isValidDirName(dirName string) bool {
// convertDirNameToPkgName converts a valid directory name to a valid package name.
// It leaves only letters and digits. All letters are mapped to lower case.
-func convertDirNameToPkgName(dirName string) source.PackageName {
+func convertDirNameToPkgName(dirName string) golang.PackageName {
var buf bytes.Buffer
for _, ch := range dirName {
switch {
@@ -331,7 +331,7 @@ func convertDirNameToPkgName(dirName string) source.PackageName {
buf.WriteRune(ch)
}
}
- return source.PackageName(buf.String())
+ return golang.PackageName(buf.String())
}
// isLetter and isDigit allow only ASCII characters because
diff --git a/gopls/internal/lsp/source/completion/package_test.go b/gopls/internal/golang/completion/package_test.go
similarity index 96%
rename from gopls/internal/lsp/source/completion/package_test.go
rename to gopls/internal/golang/completion/package_test.go
index 614359fa5dc..dc4058fa651 100644
--- a/gopls/internal/lsp/source/completion/package_test.go
+++ b/gopls/internal/golang/completion/package_test.go
@@ -7,7 +7,7 @@ package completion
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
)
func TestIsValidDirName(t *testing.T) {
@@ -55,7 +55,7 @@ func TestIsValidDirName(t *testing.T) {
func TestConvertDirNameToPkgName(t *testing.T) {
tests := []struct {
dirName string
- pkgName source.PackageName
+ pkgName golang.PackageName
}{
{dirName: "a", pkgName: "a"},
{dirName: "abcdef", pkgName: "abcdef"},
diff --git a/gopls/internal/lsp/source/completion/postfix_snippets.go b/gopls/internal/golang/completion/postfix_snippets.go
similarity index 98%
rename from gopls/internal/lsp/source/completion/postfix_snippets.go
rename to gopls/internal/golang/completion/postfix_snippets.go
index 0490b386161..252d2e77a90 100644
--- a/gopls/internal/lsp/source/completion/postfix_snippets.go
+++ b/gopls/internal/golang/completion/postfix_snippets.go
@@ -16,10 +16,10 @@ import (
"sync"
"text/template"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "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/snippet"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/golang/completion/snippet"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
@@ -465,7 +465,7 @@ func (a *postfixTmplArgs) VarName(t types.Type, nonNamedDefault string) string {
// go/types predicates are undefined on types.Typ[types.Invalid].
if !types.Identical(t, types.Typ[types.Invalid]) && types.Implements(t, errorIntf) {
name = "err"
- } else if _, isNamed := source.Deref(t).(*types.Named); !isNamed {
+ } else if _, isNamed := golang.Deref(t).(*types.Named); !isNamed {
name = nonNamedDefault
}
@@ -582,7 +582,7 @@ func (c *completer) addPostfixSnippetCandidates(ctx context.Context, sel *ast.Se
}
tmplArgs := postfixTmplArgs{
- X: source.FormatNode(c.pkg.FileSet(), sel.X),
+ X: golang.FormatNode(c.pkg.FileSet(), sel.X),
StmtOK: stmtOK,
Obj: exprObj(c.pkg.GetTypesInfo(), sel.X),
Type: selType,
diff --git a/gopls/internal/lsp/source/completion/printf.go b/gopls/internal/golang/completion/printf.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/printf.go
rename to gopls/internal/golang/completion/printf.go
diff --git a/gopls/internal/lsp/source/completion/printf_test.go b/gopls/internal/golang/completion/printf_test.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/printf_test.go
rename to gopls/internal/golang/completion/printf_test.go
diff --git a/gopls/internal/lsp/source/completion/snippet.go b/gopls/internal/golang/completion/snippet.go
similarity index 91%
rename from gopls/internal/lsp/source/completion/snippet.go
rename to gopls/internal/golang/completion/snippet.go
index c37736254aa..8df81f87672 100644
--- a/gopls/internal/lsp/source/completion/snippet.go
+++ b/gopls/internal/golang/completion/snippet.go
@@ -7,7 +7,7 @@ package completion
import (
"go/ast"
- "golang.org/x/tools/gopls/internal/lsp/source/completion/snippet"
+ "golang.org/x/tools/gopls/internal/golang/completion/snippet"
"golang.org/x/tools/gopls/internal/util/safetoken"
)
@@ -50,6 +50,11 @@ func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snip
}
// functionCallSnippet calculates the snippet for function calls.
+//
+// Callers should omit the suffix of type parameters that are
+// constrained by the argument types, to avoid offering completions
+// that contain instantiations that are redundant because of type
+// inference, such as f[int](1) for func f[T any](x T).
func (c *completer) functionCallSnippet(name string, tparams, params []string, snip *snippet.Builder) {
if !c.opts.completeFunctionCalls {
snip.WriteText(name)
diff --git a/gopls/internal/lsp/source/completion/snippet/snippet_builder.go b/gopls/internal/golang/completion/snippet/snippet_builder.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/snippet/snippet_builder.go
rename to gopls/internal/golang/completion/snippet/snippet_builder.go
diff --git a/gopls/internal/lsp/source/completion/snippet/snippet_builder_test.go b/gopls/internal/golang/completion/snippet/snippet_builder_test.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/snippet/snippet_builder_test.go
rename to gopls/internal/golang/completion/snippet/snippet_builder_test.go
diff --git a/gopls/internal/lsp/source/completion/statements.go b/gopls/internal/golang/completion/statements.go
similarity index 96%
rename from gopls/internal/lsp/source/completion/statements.go
rename to gopls/internal/golang/completion/statements.go
index 029766d2cb8..5a945d66c15 100644
--- a/gopls/internal/lsp/source/completion/statements.go
+++ b/gopls/internal/golang/completion/statements.go
@@ -11,10 +11,10 @@ import (
"go/types"
"strings"
- "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/snippet"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/golang/completion/snippet"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// addStatementCandidates adds full statement completion candidates
@@ -81,7 +81,7 @@ func (c *completer) addAssignAppend() {
}
// The name or our slice is whatever's in the LHS expression.
- sliceText = source.FormatNode(fset, n.Lhs[exprIdx])
+ sliceText = golang.FormatNode(fset, n.Lhs[exprIdx])
case *ast.SelectorExpr:
// Make sure we are a selector at the beginning of a statement.
if _, parentIsExprtStmt := c.path[2].(*ast.ExprStmt); !parentIsExprtStmt {
@@ -91,7 +91,7 @@ func (c *completer) addAssignAppend() {
// So far we only know the first part of our slice name. For
// example in "s.a<>" we only know our slice begins with "s."
// since the user could still be typing.
- sliceText = source.FormatNode(fset, n.X) + "."
+ sliceText = golang.FormatNode(fset, n.X) + "."
needsLHS = true
case *ast.ExprStmt:
needsLHS = true
@@ -212,7 +212,7 @@ func (c *completer) addErrCheck() {
var (
// errVar is e.g. "err" in "foo, err := bar()".
- errVar = source.FormatNode(c.pkg.FileSet(), lastAssignee)
+ errVar = golang.FormatNode(c.pkg.FileSet(), lastAssignee)
// Whether we need to include the "if" keyword in our candidate.
needsIf = true
diff --git a/gopls/internal/lsp/source/completion/util.go b/gopls/internal/golang/completion/util.go
similarity index 97%
rename from gopls/internal/lsp/source/completion/util.go
rename to gopls/internal/golang/completion/util.go
index 82c56e05775..65d1b4d96dc 100644
--- a/gopls/internal/lsp/source/completion/util.go
+++ b/gopls/internal/golang/completion/util.go
@@ -10,8 +10,8 @@ import (
"go/types"
"golang.org/x/tools/go/types/typeutil"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/diff"
)
@@ -38,7 +38,7 @@ func eachField(T types.Type, fn func(*types.Var)) {
var visit func(T types.Type)
visit = func(T types.Type) {
- if T, ok := source.Deref(T).Underlying().(*types.Struct); ok {
+ if T, ok := golang.Deref(T).Underlying().(*types.Struct); ok {
if seen.At(T) != nil {
return
}
@@ -120,7 +120,7 @@ func resolveInvalid(fset *token.FileSet, obj types.Object, node ast.Node, info *
}
})
// Construct a fake type for the object and return a fake object with this type.
- typename := source.FormatNode(fset, resultExpr)
+ typename := golang.FormatNode(fset, resultExpr)
typ := types.NewNamed(types.NewTypeName(token.NoPos, obj.Pkg(), typename, nil), types.Typ[types.Invalid], nil)
return types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), typ)
}
diff --git a/gopls/internal/lsp/source/completion/util_test.go b/gopls/internal/golang/completion/util_test.go
similarity index 100%
rename from gopls/internal/lsp/source/completion/util_test.go
rename to gopls/internal/golang/completion/util_test.go
diff --git a/gopls/internal/lsp/source/definition.go b/gopls/internal/golang/definition.go
similarity index 93%
rename from gopls/internal/lsp/source/definition.go
rename to gopls/internal/golang/definition.go
index d8038748c8e..4cc522978e6 100644
--- a/gopls/internal/lsp/source/definition.go
+++ b/gopls/internal/golang/definition.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -13,18 +13,18 @@ import (
"go/token"
"go/types"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/event"
)
// Definition handles the textDocument/definition request for Go files.
func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) {
- ctx, done := event.Start(ctx, "source.Definition")
+ ctx, done := event.Start(ctx, "golang.Definition")
defer done()
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
@@ -118,7 +118,9 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object
astObj := file.Scope.Lookup(name)
if astObj == nil {
// Every built-in should have documentation syntax.
- return nil, bug.Errorf("internal error: no object for %s", name)
+ // However, it is possible to reach this statement by
+ // commenting out declarations in {builtin,unsafe}.go.
+ return nil, fmt.Errorf("internal error: no object for %s", name)
}
decl, ok := astObj.Decl.(ast.Node)
if !ok {
@@ -150,8 +152,10 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object
if err != nil {
return nil, nil, err
}
-
decl, err = getDecl(pgf.File, obj.Name())
+ if err != nil {
+ return nil, nil, err
+ }
} else {
// pseudo-package "builtin":
// use parsed $GOROOT/src/builtin/builtin.go
@@ -163,7 +167,9 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object
if obj.Parent() == types.Universe {
// built-in function or type
decl, err = getDecl(pgf.File, obj.Name())
-
+ if err != nil {
+ return nil, nil, err
+ }
} else if obj.Name() == "Error" {
// error.Error method
decl, err = getDecl(pgf.File, "error")
@@ -194,7 +200,7 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object
// than the var (field) object.
//
// TODO(rfindley): this function exists to preserve the pre-existing behavior
-// of source.Identifier. Eliminate this helper in favor of sharing
+// of golang.Identifier. Eliminate this helper in favor of sharing
// functionality with objectsAt, after choosing suitable primitives.
func referencedObject(pkg *cache.Package, pgf *ParsedGoFile, pos token.Pos) (*ast.Ident, types.Object, types.Type) {
path := pathEnclosingObjNode(pgf.File, pos)
diff --git a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/golang/diagnostics.go
similarity index 79%
rename from gopls/internal/lsp/source/diagnostics.go
rename to gopls/internal/golang/diagnostics.go
index d2e9332dad2..b0fa8daf83c 100644
--- a/gopls/internal/lsp/source/diagnostics.go
+++ b/gopls/internal/golang/diagnostics.go
@@ -2,14 +2,15 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
- "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/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/progress"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/util/maps"
)
@@ -18,7 +19,7 @@ import (
//
// If the provided tracker is non-nil, it may be used to provide notifications
// of the ongoing analysis pass.
-func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID]unit, tracker *progress.Tracker) (map[protocol.DocumentURI][]*cache.Diagnostic, error) {
+func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID]*metadata.Package, tracker *progress.Tracker) (map[protocol.DocumentURI][]*cache.Diagnostic, error) {
// Exit early if the context has been canceled. This also protects us
// from a race on Options, see golang/go#36699.
if ctx.Err() != nil {
@@ -29,7 +30,6 @@ func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID
categories := []map[string]*settings.Analyzer{
options.DefaultAnalyzers,
options.StaticcheckAnalyzers,
- options.TypeErrorAnalyzers,
}
var analyzers []*settings.Analyzer
diff --git a/gopls/internal/lsp/source/embeddirective.go b/gopls/internal/golang/embeddirective.go
similarity index 98%
rename from gopls/internal/lsp/source/embeddirective.go
rename to gopls/internal/golang/embeddirective.go
index e656378198d..485da5c7a2d 100644
--- a/gopls/internal/lsp/source/embeddirective.go
+++ b/gopls/internal/golang/embeddirective.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"errors"
@@ -14,7 +14,7 @@ import (
"unicode"
"unicode/utf8"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// ErrNoEmbed is returned by EmbedDefinition when no embed
diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/golang/extract.go
similarity index 99%
rename from gopls/internal/lsp/source/extract.go
rename to gopls/internal/golang/extract.go
index 0cc1950cad3..c07faec1b7a 100644
--- a/gopls/internal/lsp/source/extract.go
+++ b/gopls/internal/golang/extract.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"bytes"
diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/golang/fix.go
similarity index 60%
rename from gopls/internal/lsp/source/fix.go
rename to gopls/internal/golang/fix.go
index 74703cf8d0a..6f07cb869c5 100644
--- a/gopls/internal/lsp/source/fix.go
+++ b/gopls/internal/golang/fix.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -12,18 +12,20 @@ import (
"go/types"
"golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/gopls/internal/analysis/embeddirective"
"golang.org/x/tools/gopls/internal/analysis/fillstruct"
+ "golang.org/x/tools/gopls/internal/analysis/stubmethods"
"golang.org/x/tools/gopls/internal/analysis/undeclaredname"
+ "golang.org/x/tools/gopls/internal/analysis/unusedparams"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/settings"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/imports"
)
-// A Fixer is a function that suggests a fix for a diagnostic produced
+// A fixer is a function that suggests a fix for a diagnostic produced
// by the analysis framework. This is done outside of the analyzer Run
// function so that the construction of expensive fixes can be
// deferred until they are requested by the user.
@@ -37,21 +39,8 @@ import (
// (SuggestedFix.TextEdits[*].{Pos,End}) must belong to the returned
// FileSet.
//
-// A Fixer may return (nil, nil) if no fix is available.
-type Fixer func(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error)
-
-// fixers maps each Fix id to its Fixer function.
-var fixers = map[settings.Fix]Fixer{
- settings.AddEmbedImport: addEmbedImport,
- settings.ExtractFunction: singleFile(extractFunction),
- settings.ExtractMethod: singleFile(extractMethod),
- settings.ExtractVariable: singleFile(extractVariable),
- settings.FillStruct: singleFile(fillstruct.SuggestedFix),
- settings.InlineCall: inlineCall,
- settings.InvertIfCondition: singleFile(invertIfCondition),
- settings.StubMethods: stubMethodsFixer,
- settings.UndeclaredName: singleFile(undeclaredname.SuggestedFix),
-}
+// A fixer may return (nil, nil) if no fix is available.
+type fixer func(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error)
// A singleFileFixer is a Fixer that inspects only a single file,
// and does not depend on data types from the cache package.
@@ -62,15 +51,71 @@ var fixers = map[settings.Fix]Fixer{
type singleFileFixer func(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error)
// singleFile adapts a single-file fixer to a Fixer.
-func singleFile(fixer singleFileFixer) Fixer {
+func singleFile(fixer1 singleFileFixer) fixer {
return func(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
- return fixer(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo())
+ return fixer1(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo())
}
}
+// Names of ApplyFix.Fix created directly by the CodeAction handler.
+const (
+ fixExtractVariable = "extract_variable"
+ fixExtractFunction = "extract_function"
+ fixExtractMethod = "extract_method"
+ fixInlineCall = "inline_call"
+ fixInvertIfCondition = "invert_if_condition"
+)
+
// ApplyFix applies the specified kind of suggested fix to the given
// file and range, returning the resulting edits.
-func ApplyFix(ctx context.Context, fix settings.Fix, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
+//
+// A fix kind is either the Category of an analysis.Diagnostic that
+// had a SuggestedFix with no edits; or the name of a fix agreed upon
+// by [CodeActions] and this function.
+// Fix kinds identify fixes in the command protocol.
+//
+// TODO(adonovan): come up with a better mechanism for registering the
+// connection between analyzers, code actions, and fixers. A flaw of
+// the current approach is that the same Category could in theory
+// apply to a Diagnostic with several lazy fixes, making them
+// impossible to distinguish. It would more precise if there was a
+// SuggestedFix.Category field, or some other way to squirrel metadata
+// in the fix.
+func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) {
+ // This can't be expressed as an entry in the fixer table below
+ // because it operates in the protocol (not go/{token,ast}) domain.
+ // (Sigh; perhaps it was a mistake to factor out the
+ // NarrowestPackageForFile/RangePos/suggestedFixToEdits
+ // steps.)
+ if fix == unusedparams.FixCategory {
+ changes, err := RemoveUnusedParameter(ctx, fh, rng, snapshot)
+ if err != nil {
+ return nil, err
+ }
+ // Unwrap TextDocumentEdits again!
+ var edits []protocol.TextDocumentEdit
+ for _, change := range changes {
+ edits = append(edits, *change.TextDocumentEdit)
+ }
+ return edits, nil
+ }
+
+ fixers := map[string]fixer{
+ // Fixes for analyzer-provided diagnostics.
+ // These match the Diagnostic.Category.
+ embeddirective.FixCategory: addEmbedImport,
+ fillstruct.FixCategory: singleFile(fillstruct.SuggestedFix),
+ stubmethods.FixCategory: stubMethodsFixer,
+ undeclaredname.FixCategory: singleFile(undeclaredname.SuggestedFix),
+
+ // Ad-hoc fixers: these are used when the command is
+ // constructed directly by logic in server/code_action.
+ fixExtractFunction: singleFile(extractFunction),
+ fixExtractMethod: singleFile(extractMethod),
+ fixExtractVariable: singleFile(extractVariable),
+ fixInlineCall: inlineCall,
+ fixInvertIfCondition: singleFile(invertIfCondition),
+ }
fixer, ok := fixers[fix]
if !ok {
return nil, fmt.Errorf("no suggested fix function for %s", fix)
@@ -93,7 +138,7 @@ func ApplyFix(ctx context.Context, fix settings.Fix, snapshot *cache.Snapshot, f
return suggestedFixToEdits(ctx, snapshot, fixFset, suggestion)
}
-// suggestedFixToEdits converts the suggestion's edits from analysis form into protocol form,
+// suggestedFixToEdits converts the suggestion's edits from analysis form into protocol form.
func suggestedFixToEdits(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, suggestion *analysis.SuggestedFix) ([]protocol.TextDocumentEdit, error) {
editsPerFile := map[protocol.DocumentURI]*protocol.TextDocumentEdit{}
for _, edit := range suggestion.TextEdits {
@@ -146,7 +191,7 @@ func suggestedFixToEdits(ctx context.Context, snapshot *cache.Snapshot, fset *to
// addEmbedImport adds a missing embed "embed" import with blank name.
func addEmbedImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, _, _ token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
- // Like source.AddImport, but with _ as Name and using our pgf.
+ // Like golang.AddImport, but with _ as Name and using our pgf.
protoEdits, err := ComputeOneImportFixEdits(snapshot, pgf, &imports.ImportFix{
StmtInfo: imports.ImportInfo{
ImportPath: "embed",
diff --git a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/golang/folding_range.go
similarity index 98%
rename from gopls/internal/lsp/source/folding_range.go
rename to gopls/internal/golang/folding_range.go
index 130f2fbbb39..c856f0a1184 100644
--- a/gopls/internal/lsp/source/folding_range.go
+++ b/gopls/internal/golang/folding_range.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -11,9 +11,9 @@ import (
"sort"
"strings"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
)
diff --git a/gopls/internal/lsp/source/format.go b/gopls/internal/golang/format.go
similarity index 85%
rename from gopls/internal/lsp/source/format.go
rename to gopls/internal/golang/format.go
index 8c469904d76..2eb2b8b01e4 100644
--- a/gopls/internal/lsp/source/format.go
+++ b/gopls/internal/golang/format.go
@@ -2,8 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Package source provides core features for use by Go editors and tools.
-package source
+// Package golang defines the LSP features for navigation, analysis,
+// and refactoring of Go source code.
+package golang
import (
"bytes"
@@ -16,9 +17,9 @@ import (
"strings"
"text/scanner"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
@@ -28,7 +29,7 @@ import (
// Format formats a file with a given range.
func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.TextEdit, error) {
- ctx, done := event.Start(ctx, "source.Format")
+ ctx, done := event.Start(ctx, "golang.Format")
defer done()
// Generated files shouldn't be edited. So, don't format them
@@ -48,7 +49,7 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr
if err != nil {
return nil, err
}
- return computeTextEdits(ctx, snapshot, pgf, string(formatted))
+ return computeTextEdits(ctx, pgf, string(formatted))
}
// format.Node changes slightly from one release to another, so the version
@@ -87,11 +88,11 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr
}
formatted = string(b)
}
- return computeTextEdits(ctx, snapshot, pgf, formatted)
+ return computeTextEdits(ctx, pgf, formatted)
}
func formatSource(ctx context.Context, fh file.Handle) ([]byte, error) {
- _, done := event.Start(ctx, "source.formatSource")
+ _, done := event.Start(ctx, "golang.formatSource")
defer done()
data, err := fh.Content()
@@ -101,21 +102,21 @@ func formatSource(ctx context.Context, fh file.Handle) ([]byte, error) {
return format.Source(data)
}
-type ImportFix struct {
- Fix *imports.ImportFix
- Edits []protocol.TextEdit
+type importFix struct {
+ fix *imports.ImportFix
+ edits []protocol.TextEdit
}
-// AllImportsFixes formats f for each possible fix to the imports.
+// allImportsFixes formats f for each possible fix to the imports.
// In addition to returning the result of applying all edits,
// it returns a list of fixes that could be applied to the file, with the
// corresponding TextEdits that would be needed to apply that fix.
-func AllImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
- ctx, done := event.Start(ctx, "source.AllImportsFixes")
+func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) {
+ ctx, done := event.Start(ctx, "golang.AllImportsFixes")
defer done()
if err := snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error {
- allFixEdits, editsPerFix, err = computeImportEdits(ctx, snapshot, pgf, opts)
+ allFixEdits, editsPerFix, err = computeImportEdits(ctx, pgf, opts)
return err
}); err != nil {
return nil, nil, fmt.Errorf("AllImportsFixes: %v", err)
@@ -125,7 +126,7 @@ func AllImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedG
// computeImportEdits computes a set of edits that perform one or all of the
// necessary import fixes.
-func computeImportEdits(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) {
+func computeImportEdits(ctx context.Context, pgf *ParsedGoFile, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) {
filename := pgf.URI.Path()
// Build up basic information about the original file.
@@ -134,7 +135,7 @@ func computeImportEdits(ctx context.Context, snapshot *cache.Snapshot, pgf *Pars
return nil, nil, err
}
- allFixEdits, err = computeFixEdits(snapshot, pgf, options, allFixes)
+ allFixEdits, err = computeFixEdits(pgf, options, allFixes)
if err != nil {
return nil, nil, err
}
@@ -142,13 +143,13 @@ func computeImportEdits(ctx context.Context, snapshot *cache.Snapshot, pgf *Pars
// Apply all of the import fixes to the file.
// Add the edits for each fix to the result.
for _, fix := range allFixes {
- edits, err := computeFixEdits(snapshot, pgf, options, []*imports.ImportFix{fix})
+ edits, err := computeFixEdits(pgf, options, []*imports.ImportFix{fix})
if err != nil {
return nil, nil, err
}
- editsPerFix = append(editsPerFix, &ImportFix{
- Fix: fix,
- Edits: edits,
+ editsPerFix = append(editsPerFix, &importFix{
+ fix: fix,
+ edits: edits,
})
}
return allFixEdits, editsPerFix, nil
@@ -166,10 +167,10 @@ func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *ParsedGoFile, fix *
TabIndent: true,
TabWidth: 8,
}
- return computeFixEdits(snapshot, pgf, options, []*imports.ImportFix{fix})
+ return computeFixEdits(pgf, options, []*imports.ImportFix{fix})
}
-func computeFixEdits(snapshot *cache.Snapshot, pgf *ParsedGoFile, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) {
+func computeFixEdits(pgf *ParsedGoFile, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) {
// trim the original data to match fixedData
left, err := importPrefix(pgf.Src)
if err != nil {
@@ -302,8 +303,8 @@ func scanForCommentEnd(src []byte) int {
return 0
}
-func computeTextEdits(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile, formatted string) ([]protocol.TextEdit, error) {
- _, done := event.Start(ctx, "source.computeTextEdits")
+func computeTextEdits(ctx context.Context, pgf *ParsedGoFile, formatted string) ([]protocol.TextEdit, error) {
+ _, done := event.Start(ctx, "golang.computeTextEdits")
defer done()
edits := diff.Strings(string(pgf.Src), formatted)
diff --git a/gopls/internal/lsp/source/format_test.go b/gopls/internal/golang/format_test.go
similarity index 99%
rename from gopls/internal/lsp/source/format_test.go
rename to gopls/internal/golang/format_test.go
index daa5b8f5dc7..4dbb4db71c0 100644
--- a/gopls/internal/lsp/source/format_test.go
+++ b/gopls/internal/golang/format_test.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"strings"
diff --git a/gopls/internal/lsp/source/gc_annotations.go b/gopls/internal/golang/gc_annotations.go
similarity index 96%
rename from gopls/internal/lsp/source/gc_annotations.go
rename to gopls/internal/golang/gc_annotations.go
index b6230ced8cc..1ff866122ca 100644
--- a/gopls/internal/lsp/source/gc_annotations.go
+++ b/gopls/internal/golang/gc_annotations.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"bytes"
@@ -13,9 +13,9 @@ import (
"path/filepath"
"strings"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/gocommand"
)
@@ -34,6 +34,7 @@ func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *me
if err != nil {
return nil, err
}
+ tmpFile.Close() // ignore error
defer os.Remove(tmpFile.Name())
outDirURI := protocol.URIFromPath(outDir)
diff --git a/gopls/internal/lsp/source/highlight.go b/gopls/internal/golang/highlight.go
similarity index 67%
rename from gopls/internal/lsp/source/highlight.go
rename to gopls/internal/golang/highlight.go
index 7e15daecb17..f11426319cf 100644
--- a/gopls/internal/lsp/source/highlight.go
+++ b/gopls/internal/golang/highlight.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -12,15 +12,15 @@ import (
"go/types"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/event"
)
func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Range, error) {
- ctx, done := event.Start(ctx, "source.Highlight")
+ ctx, done := event.Start(ctx, "golang.Highlight")
defer done()
// We always want fully parsed files for highlight, regardless
@@ -65,6 +65,8 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
return ranges, nil
}
+// highlightPath returns ranges to highlight for the given enclosing path,
+// which should be the result of astutil.PathEnclosingInterval.
func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]struct{}, error) {
result := make(map[posRange]struct{})
switch node := path[0].(type) {
@@ -133,116 +135,170 @@ type posRange struct {
start, end token.Pos
}
-func highlightFuncControlFlow(path []ast.Node, result map[posRange]struct{}) {
- var enclosingFunc ast.Node
- var returnStmt *ast.ReturnStmt
- var resultsList *ast.FieldList
- inReturnList := false
-
-Outer:
- // Reverse walk the path till we get to the func block.
+// highlightFuncControlFlow adds highlight ranges to the result map to
+// associate results and result parameters.
+//
+// Specifically, if the cursor is in a result or result parameter, all
+// results and result parameters with the same index are highlighted. If the
+// cursor is in a 'func' or 'return' keyword, the func keyword as well as all
+// returns from that func are highlighted.
+//
+// As a special case, if the cursor is within a complicated expression, control
+// flow highlighting is disabled, as it would highlight too much.
+func highlightFuncControlFlow(path []ast.Node, result map[posRange]unit) {
+
+ var (
+ funcType *ast.FuncType // type of enclosing func, or nil
+ funcBody *ast.BlockStmt // body of enclosing func, or nil
+ returnStmt *ast.ReturnStmt // enclosing ReturnStmt within the func, or nil
+ )
+
+findEnclosingFunc:
for i, n := range path {
- switch node := n.(type) {
+ switch n := n.(type) {
+ // TODO(rfindley, low priority): these pre-existing cases for KeyValueExpr
+ // and CallExpr appear to avoid highlighting when the cursor is in a
+ // complicated expression. However, the basis for this heuristic is
+ // unclear. Can we formalize a rationale?
case *ast.KeyValueExpr:
- // If cursor is in a key: value expr, we don't want control flow highlighting
+ // If cursor is in a key: value expr, we don't want control flow highlighting.
return
+
case *ast.CallExpr:
// If cursor is an arg in a callExpr, we don't want control flow highlighting.
if i > 0 {
- for _, arg := range node.Args {
+ for _, arg := range n.Args {
if arg == path[i-1] {
return
}
}
}
- case *ast.Field:
- inReturnList = true
+
case *ast.FuncLit:
- enclosingFunc = n
- resultsList = node.Type.Results
- break Outer
+ funcType = n.Type
+ funcBody = n.Body
+ break findEnclosingFunc
+
case *ast.FuncDecl:
- enclosingFunc = n
- resultsList = node.Type.Results
- break Outer
+ funcType = n.Type
+ funcBody = n.Body
+ break findEnclosingFunc
+
case *ast.ReturnStmt:
- returnStmt = node
- // If the cursor is not directly in a *ast.ReturnStmt, then
- // we need to know if it is within one of the values that is being returned.
- inReturnList = inReturnList || path[0] != returnStmt
- }
- }
- // Cursor is not in a function.
- if enclosingFunc == nil {
- return
- }
- // If the cursor is on a "return" or "func" keyword, we should highlight all of the exit
- // points of the function, including the "return" and "func" keywords.
- highlightAllReturnsAndFunc := path[0] == returnStmt || path[0] == enclosingFunc
- switch path[0].(type) {
- case *ast.Ident, *ast.BasicLit:
- // Cursor is in an identifier and not in a return statement or in the results list.
- if returnStmt == nil && !inReturnList {
- return
+ returnStmt = n
}
- case *ast.FuncType:
- highlightAllReturnsAndFunc = true
}
- // The user's cursor may be within the return statement of a function,
- // or within the result section of a function's signature.
- // index := -1
- var nodes []ast.Node
- if returnStmt != nil {
- for _, n := range returnStmt.Results {
- nodes = append(nodes, n)
- }
- } else if resultsList != nil {
- for _, n := range resultsList.List {
- nodes = append(nodes, n)
- }
- }
- _, index := nodeAtPos(nodes, path[0].Pos())
- // Highlight the correct argument in the function declaration return types.
- if resultsList != nil && -1 < index && index < len(resultsList.List) {
- rng := posRange{
- start: resultsList.List[index].Pos(),
- end: resultsList.List[index].End(),
- }
- result[rng] = struct{}{}
+ if funcType == nil {
+ return // cursor is not in a function
}
- // Add the "func" part of the func declaration.
- if highlightAllReturnsAndFunc {
- r := posRange{
- start: enclosingFunc.Pos(),
- end: enclosingFunc.Pos() + token.Pos(len("func")),
+
+ // Helper functions for inspecting the current location.
+ var (
+ pos = path[0].Pos()
+ inSpan = func(start, end token.Pos) bool { return start <= pos && pos < end }
+ inNode = func(n ast.Node) bool { return inSpan(n.Pos(), n.End()) }
+ )
+
+ inResults := funcType.Results != nil && inNode(funcType.Results)
+
+ // If the cursor is on a "return" or "func" keyword, but not highlighting any
+ // specific field or expression, we should highlight all of the exit points
+ // of the function, including the "return" and "func" keywords.
+ funcEnd := funcType.Func + token.Pos(len("func"))
+ highlightAll := path[0] == returnStmt || inSpan(funcType.Func, funcEnd)
+ var highlightIndexes map[int]bool
+
+ if highlightAll {
+ // Add the "func" part of the func declaration.
+ result[posRange{
+ start: funcType.Func,
+ end: funcEnd,
+ }] = unit{}
+ } else if returnStmt == nil && !inResults {
+ return // nothing to highlight
+ } else {
+ // If we're not highighting the entire return statement, we need to collect
+ // specific result indexes to highlight. This may be more than one index if
+ // the cursor is on a multi-name result field, but not in any specific name.
+ if !highlightAll {
+ highlightIndexes = make(map[int]bool)
+ if returnStmt != nil {
+ for i, n := range returnStmt.Results {
+ if inNode(n) {
+ highlightIndexes[i] = true
+ break
+ }
+ }
+ }
+
+ // Scan fields, either adding highlights according to the highlightIndexes
+ // computed above, or accounting for the cursor position within the result
+ // list.
+ // (We do both at once to avoid repeating the cumbersome field traversal.)
+ i := 0
+ findField:
+ for _, field := range funcType.Results.List {
+ for j, name := range field.Names {
+ if inNode(name) || highlightIndexes[i+j] {
+ result[posRange{name.Pos(), name.End()}] = unit{}
+ highlightIndexes[i+j] = true
+ break findField // found/highlighted the specific name
+ }
+ }
+ // If the cursor is in a field but not in a name (e.g. in the space, or
+ // the type), highlight the whole field.
+ //
+ // Note that this may not be ideal if we're at e.g.
+ //
+ // (x,‸y int, z int8)
+ //
+ // ...where it would make more sense to highlight only y. But we don't
+ // reach this function if not in a func, return, ident, or basiclit.
+ if inNode(field) || highlightIndexes[i] {
+ result[posRange{field.Pos(), field.End()}] = unit{}
+ highlightIndexes[i] = true
+ if inNode(field) {
+ for j := range field.Names {
+ highlightIndexes[i+j] = true
+ }
+ }
+ break findField // found/highlighted the field
+ }
+
+ n := len(field.Names)
+ if n == 0 {
+ n = 1
+ }
+ i += n
+ }
}
- result[r] = struct{}{}
}
- ast.Inspect(enclosingFunc, func(n ast.Node) bool {
- // Don't traverse any other functions.
- switch n.(type) {
- case *ast.FuncDecl, *ast.FuncLit:
- return enclosingFunc == n
- }
- ret, ok := n.(*ast.ReturnStmt)
- if !ok {
+
+ if funcBody != nil {
+ ast.Inspect(funcBody, func(n ast.Node) bool {
+ switch n := n.(type) {
+ case *ast.FuncDecl, *ast.FuncLit:
+ // Don't traverse into any functions other than enclosingFunc.
+ return false
+ case *ast.ReturnStmt:
+ if highlightAll {
+ // Add the entire return statement.
+ result[posRange{n.Pos(), n.End()}] = unit{}
+ } else {
+ // Add the highlighted indexes.
+ for i, expr := range n.Results {
+ if highlightIndexes[i] {
+ result[posRange{expr.Pos(), expr.End()}] = unit{}
+ }
+ }
+ }
+ return false
+
+ }
return true
- }
- var toAdd ast.Node
- // Add the entire return statement, applies when highlight the word "return" or "func".
- if highlightAllReturnsAndFunc {
- toAdd = n
- }
- // Add the relevant field within the entire return statement.
- if -1 < index && index < len(ret.Results) {
- toAdd = ret.Results[index]
- }
- if toAdd != nil {
- result[posRange{start: toAdd.Pos(), end: toAdd.End()}] = struct{}{}
- }
- return false
- })
+ })
+ }
}
// highlightUnlabeledBreakFlow highlights the innermost enclosing for/range/switch or swlect
diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/golang/hover.go
similarity index 79%
rename from gopls/internal/lsp/source/hover.go
rename to gopls/internal/golang/hover.go
index 1da3ab59cc6..ef486018786 100644
--- a/gopls/internal/lsp/source/hover.go
+++ b/gopls/internal/golang/hover.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"bytes"
@@ -19,27 +19,34 @@ import (
"path/filepath"
"strconv"
"strings"
+ "text/tabwriter"
"time"
"unicode/utf8"
"golang.org/x/text/unicode/runenames"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/gopls/internal/util/slices"
+ "golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/tokeninternal"
)
-// HoverJSON contains information used by hover. It is also the JSON returned
-// for the "structured" hover format
-type HoverJSON struct {
+// hoverJSON contains the structured result of a hover query. It is
+// formatted in one of several formats as determined by the HoverKind
+// setting, one of which is JSON.
+//
+// We believe this is used only by govim.
+// TODO(adonovan): see if we can wean all clients of this interface.
+type hoverJSON struct {
// Synopsis is a single sentence synopsis of the symbol's documentation.
Synopsis string `json:"synopsis"`
@@ -63,11 +70,31 @@ type HoverJSON struct {
// LinkAnchor is the pkg.go.dev link anchor for the given symbol.
// For example, the "Node" part of "pkg.go.dev/go/ast#Node".
LinkAnchor string `json:"linkAnchor"`
+
+ // New fields go below, and are unexported. The existing
+ // exported fields are underspecified and have already
+ // constrained our movements too much. A detailed JSON
+ // interface might be nice, but it needs a design and a
+ // precise specification.
+
+ // typeDecl is the declaration syntax for a type,
+ // or "" for a non-type.
+ typeDecl string
+
+ // methods is the list of descriptions of methods of a type,
+ // omitting any that are obvious from typeDecl.
+ // It is "" for a non-type.
+ methods string
+
+ // promotedFields is the list of descriptions of accessible
+ // fields of a (struct) type that were promoted through an
+ // embedded field.
+ promotedFields string
}
// Hover implements the "textDocument/hover" RPC for Go files.
func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
- ctx, done := event.Start(ctx, "source.Hover")
+ ctx, done := event.Start(ctx, "golang.Hover")
defer done()
rng, h, err := hover(ctx, snapshot, fh, position)
@@ -93,7 +120,7 @@ func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, positi
// hover computes hover information at the given position. If we do not support
// hovering at the position, it returns _, nil, nil: an error is only returned
// if the position is valid but we fail to compute hover information.
-func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) (protocol.Range, *HoverJSON, error) {
+func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) (protocol.Range, *hoverJSON, error) {
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil {
return protocol.Range{}, nil, err
@@ -169,7 +196,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
// By convention, we qualify hover information relative to the package
// from which the request originated.
- qf := Qualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo())
+ qf := typesutil.FileQualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo())
// Handle type switch identifiers as a special case, since they don't have an
// object.
@@ -178,7 +205,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
if selectedType != nil {
fakeObj := types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), selectedType)
signature := types.ObjectString(fakeObj, qf)
- return rng, &HoverJSON{
+ return rng, &hoverJSON{
Signature: signature,
SingleLine: signature,
SymbolName: fakeObj.Name(),
@@ -199,7 +226,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
if err != nil {
return protocol.Range{}, nil, fmt.Errorf("re-parsing declaration of %s: %v", obj.Name(), err)
}
- decl, spec, field := findDeclInfo([]*ast.File{declPGF.File}, declPos)
+ decl, spec, field := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^3
comment := chooseDocComment(decl, spec, field)
docText := comment.Text()
@@ -214,53 +241,115 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
}
}
+ var typeDecl, methods, fields string
+
// For "objects defined by a type spec", the signature produced by
// objectString is insufficient:
// (1) large structs are formatted poorly, with no newlines
// (2) we lose inline comments
- //
// Furthermore, we include a summary of their method set.
- //
- // TODO(rfindley): this should use FormatVarType to get proper qualification
- // of identifiers, and we should revisit the formatting of method set.
- //
- // TODO(adonovan): this logic belongs in objectString.
_, isTypeName := obj.(*types.TypeName)
_, isTypeParam := obj.Type().(*types.TypeParam)
if isTypeName && !isTypeParam {
spec, ok := spec.(*ast.TypeSpec)
if !ok {
- return protocol.Range{}, nil, bug.Errorf("type name %q without type spec", obj.Name())
+ // We cannot find a TypeSpec for this type or alias declaration
+ // (that is not a type parameter or a built-in).
+ // This should be impossible even for ill-formed trees;
+ // we suspect that AST repair may be creating inconsistent
+ // positions. Don't report a bug in that case. (#64241)
+ errorf := fmt.Errorf
+ if !declPGF.Fixed() {
+ errorf = bug.Errorf
+ }
+ return protocol.Range{}, nil, errorf("type name %q without type spec", obj.Name())
}
- spec2 := *spec
- // Don't duplicate comments when formatting type specs.
- spec2.Doc = nil
- spec2.Comment = nil
- var b strings.Builder
- b.WriteString("type ")
- fset := tokeninternal.FileSetFor(declPGF.Tok)
- if err := format.Node(&b, fset, &spec2); err != nil {
- return protocol.Range{}, nil, err
+
+ // Format the type's declaration syntax.
+ {
+ // Don't duplicate comments.
+ spec2 := *spec
+ spec2.Doc = nil
+ spec2.Comment = nil
+
+ var b strings.Builder
+ b.WriteString("type ")
+ fset := tokeninternal.FileSetFor(declPGF.Tok)
+ // TODO(adonovan): use a smarter formatter that omits
+ // inaccessible fields (non-exported ones from other packages).
+ if err := format.Node(&b, fset, &spec2); err != nil {
+ return protocol.Range{}, nil, err
+ }
+ typeDecl = b.String()
}
- // Display the declared methods accessible from the identifier.
+ // Promoted fields
+ //
+ // Show a table of accessible fields of the (struct)
+ // type that may not be visible in the syntax (above)
+ // due to promotion through embedded fields.
+ //
+ // Example:
//
- // (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.)
- if !types.IsInterface(obj.Type()) {
- sep := "\n\n"
- for _, m := range typeutil.IntuitiveMethodSet(obj.Type(), nil) {
- // Show direct methods that are either exported, or defined in the
- // current package.
- if (m.Obj().Exported() || m.Obj().Pkg() == pkg.GetTypes()) && len(m.Index()) == 1 {
- b.WriteString(sep)
- sep = "\n"
- b.WriteString(types.ObjectString(m.Obj(), qf))
+ // // Embedded fields:
+ // foo int // through x.y
+ // z string // through x.y
+ if prom := promotedFields(obj.Type(), pkg.GetTypes()); len(prom) > 0 {
+ var b strings.Builder
+ b.WriteString("// Embedded fields:\n")
+ w := tabwriter.NewWriter(&b, 0, 8, 1, ' ', 0)
+ for _, f := range prom {
+ fmt.Fprintf(w, "%s\t%s\t// through %s\t\n",
+ f.field.Name(),
+ types.TypeString(f.field.Type(), qf),
+ f.path)
+ }
+ w.Flush()
+ b.WriteByte('\n')
+ fields = b.String()
+ }
+
+ // -- methods --
+
+ // For an interface type, explicit methods will have
+ // already been displayed when the node was formatted
+ // above. Don't list these again.
+ var skip map[string]bool
+ if iface, ok := spec.Type.(*ast.InterfaceType); ok {
+ if iface.Methods.List != nil {
+ for _, m := range iface.Methods.List {
+ if len(m.Names) == 1 {
+ if skip == nil {
+ skip = make(map[string]bool)
+ }
+ skip[m.Names[0].Name] = true
+ }
}
}
}
- signature = b.String()
+
+ // Display all the type's accessible methods,
+ // including those that require a pointer receiver,
+ // and those promoted from embedded struct fields or
+ // embedded interfaces.
+ var b strings.Builder
+ for _, m := range typeutil.IntuitiveMethodSet(obj.Type(), nil) {
+ if !accessibleTo(m.Obj(), pkg.GetTypes()) {
+ continue // inaccessible
+ }
+ if skip[m.Obj().Name()] {
+ continue // redundant with format.Node above
+ }
+ if b.Len() > 0 {
+ b.WriteByte('\n')
+ }
+
+ // Use objectString for its prettier rendering of method receivers.
+ b.WriteString(objectString(m.Obj(), qf, token.NoPos, nil, nil))
+ }
+ methods = b.String()
+
+ signature = typeDecl + "\n" + methods
}
// Compute link data (on pkg.go.dev or other documentation host).
@@ -363,7 +452,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1)
}
- return rng, &HoverJSON{
+ return rng, &hoverJSON{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
SingleLine: singleLineSignature,
@@ -371,18 +460,21 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
Signature: signature,
LinkPath: linkPath,
LinkAnchor: anchor,
+ typeDecl: typeDecl,
+ methods: methods,
+ promotedFields: fields,
}, nil
}
// hoverBuiltin computes hover information when hovering over a builtin
// identifier.
-func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*HoverJSON, error) {
+func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*hoverJSON, error) {
// Special handling for error.Error, which is the only builtin method.
//
// TODO(rfindley): can this be unified with the handling below?
if obj.Name() == "Error" {
signature := obj.String()
- return &HoverJSON{
+ return &hoverJSON{
Signature: signature,
SingleLine: signature,
// TODO(rfindley): these are better than the current behavior.
@@ -423,7 +515,7 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec
signature = replacer.Replace(signature)
docText := comment.Text()
- return &HoverJSON{
+ return &hoverJSON{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
Signature: signature,
@@ -438,7 +530,7 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec
// imp in the file pgf of pkg.
//
// If we do not have metadata for the hovered import, it returns _
-func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, imp *ast.ImportSpec) (protocol.Range, *HoverJSON, error) {
+func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, imp *ast.ImportSpec) (protocol.Range, *hoverJSON, error) {
rng, err := pgf.NodeRange(imp.Path)
if err != nil {
return protocol.Range{}, nil, err
@@ -481,7 +573,7 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa
}
docText := comment.Text()
- return rng, &HoverJSON{
+ return rng, &hoverJSON{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
}, nil
@@ -489,7 +581,7 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa
// hoverPackageName computes hover information for the package name of the file
// pgf in pkg.
-func hoverPackageName(pkg *cache.Package, pgf *ParsedGoFile) (protocol.Range, *HoverJSON, error) {
+func hoverPackageName(pkg *cache.Package, pgf *ParsedGoFile) (protocol.Range, *hoverJSON, error) {
var comment *ast.CommentGroup
for _, pgf := range pkg.CompiledGoFiles() {
if pgf.File.Doc != nil {
@@ -502,7 +594,7 @@ func hoverPackageName(pkg *cache.Package, pgf *ParsedGoFile) (protocol.Range, *H
return protocol.Range{}, nil, err
}
docText := comment.Text()
- return rng, &HoverJSON{
+ return rng, &hoverJSON{
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
// Note: including a signature is redundant, since the cursor is already on the
@@ -517,7 +609,7 @@ func hoverPackageName(pkg *cache.Package, pgf *ParsedGoFile) (protocol.Range, *H
// For example, hovering over "\u2211" in "foo \u2211 bar" yields:
//
// '∑', U+2211, N-ARY SUMMATION
-func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *HoverJSON, error) {
+func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverJSON, error) {
var (
value string // if non-empty, a constant value to format in hover
r rune // if non-zero, format a description of this rune in hover
@@ -630,7 +722,7 @@ func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Ran
fmt.Fprintf(&b, "U+%04X, %s", r, runeName)
}
hover := b.String()
- return rng, &HoverJSON{
+ return rng, &hoverJSON{
Synopsis: hover,
FullDocumentation: hover,
}, nil
@@ -638,7 +730,7 @@ func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Ran
// hoverEmbed computes hover information for a filepath.Match pattern.
// Assumes that the pattern is relative to the location of fh.
-func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Range, *HoverJSON, error) {
+func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Range, *hoverJSON, error) {
s := &strings.Builder{}
dir := filepath.Dir(fh.URI().Path())
@@ -670,7 +762,7 @@ func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Ra
fmt.Fprintf(s, "%s\n\n", m)
}
- json := &HoverJSON{
+ json := &hoverJSON{
Signature: fmt.Sprintf("Embedding %q", pattern),
Synopsis: s.String(),
FullDocumentation: s.String(),
@@ -894,54 +986,67 @@ func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSe
return pgf, fullPos, nil
}
-func formatHover(h *HoverJSON, options *settings.Options) (string, error) {
- signature := formatSignature(h, options)
+func formatHover(h *hoverJSON, options *settings.Options) (string, error) {
+ maybeMarkdown := func(s string) string {
+ if s != "" && options.PreferredContentFormat == protocol.Markdown {
+ s = fmt.Sprintf("```go\n%s\n```", strings.Trim(s, "\n"))
+ }
+ return s
+ }
switch options.HoverKind {
case settings.SingleLine:
return h.SingleLine, nil
+
case settings.NoDocumentation:
- return signature, nil
+ return maybeMarkdown(h.Signature), nil
+
case settings.Structured:
b, err := json.Marshal(h)
if err != nil {
return "", err
}
return string(b), nil
- }
- link := formatLink(h, options)
- doc := formatDoc(h, options)
+ case settings.SynopsisDocumentation,
+ settings.FullDocumentation:
+ // For types, we display TypeDecl and Methods,
+ // but not Signature, which is redundant (= TypeDecl + "\n" + Methods).
+ // For all other symbols, we display Signature;
+ // TypeDecl and Methods are empty.
+ // (This awkwardness is to preserve JSON compatibility.)
+ parts := []string{
+ maybeMarkdown(h.Signature),
+ maybeMarkdown(h.typeDecl),
+ formatDoc(h, options),
+ maybeMarkdown(h.promotedFields),
+ maybeMarkdown(h.methods),
+ formatLink(h, options),
+ }
+ if h.typeDecl != "" {
+ parts[0] = "" // type: suppress redundant Signature
+ }
+ parts = slices.Remove(parts, "")
- var b strings.Builder
- parts := []string{signature, doc, link}
- for i, el := range parts {
- if el != "" {
- b.WriteString(el)
-
- // If any elements of the remainder of the list are non-empty,
- // write an extra newline.
- if anyNonEmpty(parts[i+1:]) {
+ var b strings.Builder
+ for i, part := range parts {
+ if i > 0 {
if options.PreferredContentFormat == protocol.Markdown {
b.WriteString("\n\n")
} else {
- b.WriteRune('\n')
+ b.WriteByte('\n')
}
}
+ b.WriteString(part)
}
- }
- return b.String(), nil
-}
+ return b.String(), nil
-func formatSignature(h *HoverJSON, options *settings.Options) string {
- signature := h.Signature
- if signature != "" && options.PreferredContentFormat == protocol.Markdown {
- signature = fmt.Sprintf("```go\n%s\n```", signature)
+ default:
+ return "", fmt.Errorf("invalid HoverKind: %v", options.HoverKind)
}
- return signature
}
-func formatLink(h *HoverJSON, options *settings.Options) string {
+func formatLink(h *hoverJSON, options *settings.Options) string {
if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" {
return ""
}
@@ -956,7 +1061,7 @@ func formatLink(h *HoverJSON, options *settings.Options) string {
}
}
-func formatDoc(h *HoverJSON, options *settings.Options) string {
+func formatDoc(h *hoverJSON, options *settings.Options) string {
var doc string
switch options.HoverKind {
case settings.SynopsisDocumentation:
@@ -970,15 +1075,6 @@ func formatDoc(h *HoverJSON, options *settings.Options) string {
return doc
}
-func anyNonEmpty(x []string) bool {
- for _, el := range x {
- if el != "" {
- return true
- }
- }
- return false
-}
-
// findDeclInfo returns the syntax nodes involved in the declaration of the
// types.Object with position pos, searching the given list of file syntax
// trees.
@@ -1105,3 +1201,74 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe
return nil, nil, nil
}
+
+type promotedField struct {
+ path string // path (e.g. "x.y" through embedded fields)
+ field *types.Var
+}
+
+// promotedFields returns the list of accessible promoted fields of a struct type t.
+// (Logic plundered from x/tools/cmd/guru/describe.go.)
+func promotedFields(t types.Type, from *types.Package) []promotedField {
+ wantField := func(f *types.Var) bool {
+ if !accessibleTo(f, from) {
+ return false
+ }
+ // Check that the field is not shadowed.
+ obj, _, _ := types.LookupFieldOrMethod(t, true, f.Pkg(), f.Name())
+ return obj == f
+ }
+
+ var fields []promotedField
+ var visit func(t types.Type, stack []*types.Named)
+ visit = func(t types.Type, stack []*types.Named) {
+ tStruct, ok := Deref(t).Underlying().(*types.Struct)
+ if !ok {
+ return
+ }
+ fieldloop:
+ for i := 0; i < tStruct.NumFields(); i++ {
+ f := tStruct.Field(i)
+
+ // Handle recursion through anonymous fields.
+ if f.Anonymous() {
+ tf := f.Type()
+ if ptr, ok := tf.(*types.Pointer); ok {
+ tf = ptr.Elem()
+ }
+ if named, ok := tf.(*types.Named); ok { // (be defensive)
+ // If we've already visited this named type
+ // on this path, break the cycle.
+ for _, x := range stack {
+ if x.Origin() == named.Origin() {
+ continue fieldloop
+ }
+ }
+ visit(f.Type(), append(stack, named))
+ }
+ }
+
+ // Save accessible promoted fields.
+ if len(stack) > 0 && wantField(f) {
+ var path strings.Builder
+ for i, t := range stack {
+ if i > 0 {
+ path.WriteByte('.')
+ }
+ path.WriteString(t.Obj().Name())
+ }
+ fields = append(fields, promotedField{
+ path: path.String(),
+ field: f,
+ })
+ }
+ }
+ }
+ visit(t, nil)
+
+ return fields
+}
+
+func accessibleTo(obj types.Object, pkg *types.Package) bool {
+ return obj.Exported() || obj.Pkg() == pkg
+}
diff --git a/gopls/internal/lsp/source/identifier.go b/gopls/internal/golang/identifier.go
similarity index 99%
rename from gopls/internal/lsp/source/identifier.go
rename to gopls/internal/golang/identifier.go
index 9e12e3fd222..28f89757057 100644
--- a/gopls/internal/lsp/source/identifier.go
+++ b/gopls/internal/golang/identifier.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"errors"
diff --git a/gopls/internal/lsp/source/identifier_test.go b/gopls/internal/golang/identifier_test.go
similarity index 99%
rename from gopls/internal/lsp/source/identifier_test.go
rename to gopls/internal/golang/identifier_test.go
index 9f5eb7df3db..b1e6d5a75a2 100644
--- a/gopls/internal/lsp/source/identifier_test.go
+++ b/gopls/internal/golang/identifier_test.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"bytes"
diff --git a/gopls/internal/lsp/source/implementation.go b/gopls/internal/golang/implementation.go
similarity index 98%
rename from gopls/internal/lsp/source/implementation.go
rename to gopls/internal/golang/implementation.go
index 9d644d15db2..cb7dadbb380 100644
--- a/gopls/internal/lsp/source/implementation.go
+++ b/gopls/internal/golang/implementation.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -17,11 +17,11 @@ import (
"sync"
"golang.org/x/sync/errgroup"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/methodsets"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/methodsets"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/event"
@@ -49,7 +49,7 @@ import (
// If the position denotes a method, the computation is applied to its
// receiver type and then its corresponding methods are returned.
func Implementation(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position) ([]protocol.Location, error) {
- ctx, done := event.Start(ctx, "source.Implementation")
+ ctx, done := event.Start(ctx, "golang.Implementation")
defer done()
locs, err := implementations(ctx, snapshot, f, pp)
diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/golang/inlay_hint.go
similarity index 97%
rename from gopls/internal/lsp/source/inlay_hint.go
rename to gopls/internal/golang/inlay_hint.go
index 41dd6709eb7..6f35c8e5786 100644
--- a/gopls/internal/lsp/source/inlay_hint.go
+++ b/gopls/internal/golang/inlay_hint.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -13,9 +13,10 @@ import (
"go/types"
"strings"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/event"
)
@@ -80,7 +81,7 @@ var AllInlayHints = map[string]*Hint{
}
func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pRng protocol.Range) ([]protocol.InlayHint, error) {
- ctx, done := event.Start(ctx, "source.InlayHint")
+ ctx, done := event.Start(ctx, "golang.InlayHint")
defer done()
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
@@ -104,7 +105,7 @@ func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pR
}
info := pkg.GetTypesInfo()
- q := Qualifier(pgf.File, pkg.GetTypes(), info)
+ q := typesutil.FileQualifier(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()
diff --git a/gopls/internal/lsp/source/inline.go b/gopls/internal/golang/inline.go
similarity index 96%
rename from gopls/internal/lsp/source/inline.go
rename to gopls/internal/golang/inline.go
index 1519ef85d80..de2a6ef75ae 100644
--- a/gopls/internal/lsp/source/inline.go
+++ b/gopls/internal/golang/inline.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
// This file defines the refactor.inline code action.
@@ -16,9 +16,9 @@ import (
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/typeutil"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
diff --git a/gopls/internal/lsp/source/inline_all.go b/gopls/internal/golang/inline_all.go
similarity index 98%
rename from gopls/internal/lsp/source/inline_all.go
rename to gopls/internal/golang/inline_all.go
index 13f61ea808a..f2bab9d6d12 100644
--- a/gopls/internal/lsp/source/inline_all.go
+++ b/gopls/internal/golang/inline_all.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -13,8 +13,8 @@ import (
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/typeutil"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/refactor/inline"
)
diff --git a/gopls/internal/lsp/source/invertifcondition.go b/gopls/internal/golang/invertifcondition.go
similarity index 99%
rename from gopls/internal/lsp/source/invertifcondition.go
rename to gopls/internal/golang/invertifcondition.go
index 75e375ad5ec..377e1ce6186 100644
--- a/gopls/internal/lsp/source/invertifcondition.go
+++ b/gopls/internal/golang/invertifcondition.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"fmt"
@@ -92,7 +92,7 @@ func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, fi
func endsWithReturn(elseBranch ast.Stmt) (bool, error) {
elseBlock, isBlockStatement := elseBranch.(*ast.BlockStmt)
if !isBlockStatement {
- return false, fmt.Errorf("Unable to figure out whether this ends with return: %T", elseBranch)
+ return false, fmt.Errorf("unable to figure out whether this ends with return: %T", elseBranch)
}
if len(elseBlock.List) == 0 {
diff --git a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/golang/known_packages.go
similarity index 97%
rename from gopls/internal/lsp/source/known_packages.go
rename to gopls/internal/golang/known_packages.go
index d6a442e7211..60a89ca0285 100644
--- a/gopls/internal/lsp/source/known_packages.go
+++ b/gopls/internal/golang/known_packages.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -13,9 +13,9 @@ import (
"sync"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
)
diff --git a/gopls/internal/lsp/source/linkname.go b/gopls/internal/golang/linkname.go
similarity index 96%
rename from gopls/internal/lsp/source/linkname.go
rename to gopls/internal/golang/linkname.go
index bcefa1092d6..2578f8d5485 100644
--- a/gopls/internal/lsp/source/linkname.go
+++ b/gopls/internal/golang/linkname.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -11,9 +11,9 @@ import (
"go/token"
"strings"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
)
diff --git a/gopls/internal/lsp/source/origin.go b/gopls/internal/golang/origin.go
similarity index 98%
rename from gopls/internal/lsp/source/origin.go
rename to gopls/internal/golang/origin.go
index 8ee467e844e..c5e84db0ceb 100644
--- a/gopls/internal/lsp/source/origin.go
+++ b/gopls/internal/golang/origin.go
@@ -5,7 +5,7 @@
//go:build !go1.19
// +build !go1.19
-package source
+package golang
import "go/types"
diff --git a/gopls/internal/lsp/source/origin_119.go b/gopls/internal/golang/origin_119.go
similarity index 98%
rename from gopls/internal/lsp/source/origin_119.go
rename to gopls/internal/golang/origin_119.go
index a249ce4b1c5..16f6ca7c065 100644
--- a/gopls/internal/lsp/source/origin_119.go
+++ b/gopls/internal/golang/origin_119.go
@@ -5,7 +5,7 @@
//go:build go1.19
// +build go1.19
-package source
+package golang
import "go/types"
diff --git a/gopls/internal/lsp/source/references.go b/gopls/internal/golang/references.go
similarity index 98%
rename from gopls/internal/lsp/source/references.go
rename to gopls/internal/golang/references.go
index f93783ecb6b..c448830a1d0 100644
--- a/gopls/internal/lsp/source/references.go
+++ b/gopls/internal/golang/references.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
// This file defines the 'references' query based on a serializable
// index constructed during type checking, thus avoiding the need to
@@ -25,11 +25,11 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/tools/go/types/objectpath"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/methodsets"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/methodsets"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/event"
@@ -62,7 +62,7 @@ type reference struct {
// definitions before uses) to the object denoted by the identifier at
// the given file/position, searching the entire workspace.
func references(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, includeDeclaration bool) ([]reference, error) {
- ctx, done := event.Start(ctx, "source.references")
+ ctx, done := event.Start(ctx, "golang.references")
defer done()
// Is the cursor within the package name declaration?
diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/golang/rename.go
similarity index 99%
rename from gopls/internal/lsp/source/rename.go
rename to gopls/internal/golang/rename.go
index 0bf8cc283ef..29485413865 100644
--- a/gopls/internal/lsp/source/rename.go
+++ b/gopls/internal/golang/rename.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
// TODO(adonovan):
//
@@ -59,11 +59,11 @@ import (
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/objectpath"
"golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/diff"
@@ -98,7 +98,7 @@ type PrepareItem struct {
// 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 *cache.Snapshot, f file.Handle, pp protocol.Position) (_ *PrepareItem, usererr, err error) {
- ctx, done := event.Start(ctx, "source.PrepareRename")
+ ctx, done := event.Start(ctx, "golang.PrepareRename")
defer done()
// Is the cursor within the package name declaration?
@@ -216,7 +216,7 @@ func checkRenamable(obj types.Object) error {
// given identifier within a package and a boolean value of true for renaming
// package and false otherwise.
func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, bool, error) {
- ctx, done := event.Start(ctx, "source.Rename")
+ ctx, done := event.Start(ctx, "golang.Rename")
defer done()
if !isValidIdentifier(newName) {
@@ -402,7 +402,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
// transitive rdeps. (The exportedness of the field's struct
// or method's receiver is irrelevant.)
transitive := false
- switch obj.(type) {
+ switch obj := obj.(type) {
case *types.TypeName:
// Renaming an exported package-level type
// requires us to inspect all transitive rdeps
@@ -418,7 +418,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle
}
case *types.Var:
- if obj.(*types.Var).IsField() {
+ if obj.IsField() {
transitive = true // field
}
diff --git a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/golang/rename_check.go
similarity index 99%
rename from gopls/internal/lsp/source/rename_check.go
rename to gopls/internal/golang/rename_check.go
index bc6b7cfcf06..11b154c4e18 100644
--- a/gopls/internal/lsp/source/rename_check.go
+++ b/gopls/internal/golang/rename_check.go
@@ -4,7 +4,7 @@
//
// Taken from golang.org/x/tools/refactor/rename.
-package source
+package golang
// This file defines the conflict-checking portion of the rename operation.
//
@@ -43,7 +43,7 @@ import (
"unicode"
"golang.org/x/tools/go/ast/astutil"
- "golang.org/x/tools/gopls/internal/lsp/cache"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/refactor/satisfy"
)
diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go
new file mode 100644
index 00000000000..181b8ce33ab
--- /dev/null
+++ b/gopls/internal/golang/semtok.go
@@ -0,0 +1,856 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package golang
+
+// This file defines the Semantic Tokens operation for Go source.
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "log"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/semtok"
+ "golang.org/x/tools/gopls/internal/util/safetoken"
+ "golang.org/x/tools/internal/event"
+)
+
+// to control comprehensive logging of decisions (gopls semtok foo.go > /dev/null shows log output)
+// semDebug should NEVER be true in checked-in code
+const semDebug = false
+
+// The LSP says that errors for the semantic token requests should only be returned
+// for exceptions (a word not otherwise defined). This code treats a too-large file
+// as an exception. On parse errors, the code does what it can.
+
+// reject full semantic token requests for large files
+const maxFullFileSize int = 100000
+
+func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng *protocol.Range) (*protocol.SemanticTokens, error) {
+ pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+
+ // Select range.
+ var start, end token.Pos
+ if rng != nil {
+ var err error
+ start, end, err = pgf.RangePos(*rng)
+ if err != nil {
+ return nil, err // e.g. invalid range
+ }
+ } else {
+ tok := pgf.Tok
+ start, end = tok.Pos(0), tok.Pos(tok.Size()) // entire file
+ }
+ if int(end-start) > maxFullFileSize {
+ err := fmt.Errorf("semantic tokens: range %s too large (%d > %d)",
+ fh.URI().Path(), end-start, maxFullFileSize)
+ return nil, err
+ }
+
+ tv := &tokenVisitor{
+ ctx: ctx,
+ metadataSource: snapshot,
+ pgf: pgf,
+ start: start,
+ end: end,
+ ti: pkg.GetTypesInfo(),
+ pkg: pkg,
+ fset: pkg.FileSet(),
+ }
+ tv.visit()
+ return &protocol.SemanticTokens{
+ Data: semtok.Encode(
+ tv.items,
+ snapshot.Options().NoSemanticString,
+ snapshot.Options().NoSemanticNumber,
+ snapshot.Options().SemanticTypes,
+ snapshot.Options().SemanticMods),
+ // For delta requests, but we've never seen any.
+ ResultID: time.Now().String(),
+ }, nil
+}
+
+type tokenVisitor struct {
+ items []semtok.Token
+ ctx context.Context // for event logging
+ // metadataSource is used to resolve imports
+ metadataSource metadata.Source
+ pgf *ParsedGoFile
+ start, end token.Pos // range of interest
+ ti *types.Info
+ pkg *cache.Package
+ fset *token.FileSet
+ // path from the root of the parse tree, used for debugging
+ stack []ast.Node
+}
+
+func (tv *tokenVisitor) visit() {
+ f := tv.pgf.File
+ // may not be in range, but harmless
+ tv.token(f.Package, len("package"), semtok.TokKeyword, nil)
+ tv.token(f.Name.NamePos, len(f.Name.Name), semtok.TokNamespace, nil)
+ inspect := func(n ast.Node) bool {
+ return tv.inspector(n)
+ }
+ for _, d := range f.Decls {
+ // only look at the decls that overlap the range
+ start, end := d.Pos(), d.End()
+ if end <= tv.start || start >= tv.end {
+ continue
+ }
+ ast.Inspect(d, inspect)
+ }
+ for _, cg := range f.Comments {
+ for _, c := range cg.List {
+ if strings.HasPrefix(c.Text, "//go:") {
+ tv.godirective(c)
+ continue
+ }
+ if !strings.Contains(c.Text, "\n") {
+ tv.token(c.Pos(), len(c.Text), semtok.TokComment, nil)
+ continue
+ }
+ tv.multiline(c.Pos(), c.End(), c.Text, semtok.TokComment)
+ }
+ }
+}
+
+func (tv *tokenVisitor) token(start token.Pos, leng int, typ semtok.TokenType, mods []string) {
+ if leng <= 0 {
+ return // vscode doesn't like 0-length Tokens
+ }
+ if !start.IsValid() {
+ // This is not worth reporting. TODO(pjw): does it still happen?
+ return
+ }
+ if start >= tv.end || start+token.Pos(leng) <= tv.start {
+ return
+ }
+ // want a line and column from start (in LSP coordinates). Ignore line directives.
+ lspRange, err := tv.pgf.PosRange(start, start+token.Pos(leng))
+ if err != nil {
+ event.Error(tv.ctx, "failed to convert to range", err)
+ return
+ }
+ if lspRange.End.Line != lspRange.Start.Line {
+ // this happens if users are typing at the end of the file, but report nothing
+ return
+ }
+ tv.items = append(tv.items, semtok.Token{
+ Line: lspRange.Start.Line,
+ Start: lspRange.Start.Character,
+ Len: lspRange.End.Character - lspRange.Start.Character, // all on one line
+ Type: typ,
+ Modifiers: mods,
+ })
+}
+
+// convert the stack to a string, for debugging
+func (tv *tokenVisitor) strStack() string {
+ msg := []string{"["}
+ for i := len(tv.stack) - 1; i >= 0; i-- {
+ s := tv.stack[i]
+ msg = append(msg, fmt.Sprintf("%T", s)[5:])
+ }
+ if len(tv.stack) > 0 {
+ loc := tv.stack[len(tv.stack)-1].Pos()
+ if _, err := safetoken.Offset(tv.pgf.Tok, loc); err != nil {
+ msg = append(msg, fmt.Sprintf("invalid position %v for %s", loc, tv.pgf.URI))
+ } else {
+ add := safetoken.Position(tv.pgf.Tok, loc)
+ nm := filepath.Base(add.Filename)
+ msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column))
+ }
+ }
+ msg = append(msg, "]")
+ return strings.Join(msg, " ")
+}
+
+// find the line in the source
+func (tv *tokenVisitor) srcLine(x ast.Node) string {
+ file := tv.pgf.Tok
+ line := safetoken.Line(file, x.Pos())
+ start, err := safetoken.Offset(file, file.LineStart(line))
+ if err != nil {
+ return ""
+ }
+ end := start
+ for ; end < len(tv.pgf.Src) && tv.pgf.Src[end] != '\n'; end++ {
+
+ }
+ ans := tv.pgf.Src[start:end]
+ return string(ans)
+}
+
+func (tv *tokenVisitor) inspector(n ast.Node) bool {
+ pop := func() {
+ tv.stack = tv.stack[:len(tv.stack)-1]
+ }
+ if n == nil {
+ pop()
+ return true
+ }
+ tv.stack = append(tv.stack, n)
+ switch x := n.(type) {
+ case *ast.ArrayType:
+ case *ast.AssignStmt:
+ tv.token(x.TokPos, len(x.Tok.String()), semtok.TokOperator, nil)
+ case *ast.BasicLit:
+ if strings.Contains(x.Value, "\n") {
+ // has to be a string.
+ tv.multiline(x.Pos(), x.End(), x.Value, semtok.TokString)
+ break
+ }
+ ln := len(x.Value)
+ what := semtok.TokNumber
+ if x.Kind == token.STRING {
+ what = semtok.TokString
+ }
+ tv.token(x.Pos(), ln, what, nil)
+ case *ast.BinaryExpr:
+ tv.token(x.OpPos, len(x.Op.String()), semtok.TokOperator, nil)
+ case *ast.BlockStmt:
+ case *ast.BranchStmt:
+ tv.token(x.TokPos, len(x.Tok.String()), semtok.TokKeyword, nil)
+ // There's no semantic encoding for labels
+ case *ast.CallExpr:
+ if x.Ellipsis != token.NoPos {
+ tv.token(x.Ellipsis, len("..."), semtok.TokOperator, nil)
+ }
+ case *ast.CaseClause:
+ iam := "case"
+ if x.List == nil {
+ iam = "default"
+ }
+ tv.token(x.Case, len(iam), semtok.TokKeyword, nil)
+ case *ast.ChanType:
+ // chan | chan <- | <- chan
+ switch {
+ case x.Arrow == token.NoPos:
+ tv.token(x.Begin, len("chan"), semtok.TokKeyword, nil)
+ case x.Arrow == x.Begin:
+ tv.token(x.Arrow, 2, semtok.TokOperator, nil)
+ pos := tv.findKeyword("chan", x.Begin+2, x.Value.Pos())
+ tv.token(pos, len("chan"), semtok.TokKeyword, nil)
+ case x.Arrow != x.Begin:
+ tv.token(x.Begin, len("chan"), semtok.TokKeyword, nil)
+ tv.token(x.Arrow, 2, semtok.TokOperator, nil)
+ }
+ case *ast.CommClause:
+ iam := len("case")
+ if x.Comm == nil {
+ iam = len("default")
+ }
+ tv.token(x.Case, iam, semtok.TokKeyword, nil)
+ case *ast.CompositeLit:
+ case *ast.DeclStmt:
+ case *ast.DeferStmt:
+ tv.token(x.Defer, len("defer"), semtok.TokKeyword, nil)
+ case *ast.Ellipsis:
+ tv.token(x.Ellipsis, len("..."), semtok.TokOperator, nil)
+ case *ast.EmptyStmt:
+ case *ast.ExprStmt:
+ case *ast.Field:
+ case *ast.FieldList:
+ case *ast.ForStmt:
+ tv.token(x.For, len("for"), semtok.TokKeyword, nil)
+ case *ast.FuncDecl:
+ case *ast.FuncLit:
+ case *ast.FuncType:
+ if x.Func != token.NoPos {
+ tv.token(x.Func, len("func"), semtok.TokKeyword, nil)
+ }
+ case *ast.GenDecl:
+ tv.token(x.TokPos, len(x.Tok.String()), semtok.TokKeyword, nil)
+ case *ast.GoStmt:
+ tv.token(x.Go, len("go"), semtok.TokKeyword, nil)
+ case *ast.Ident:
+ tv.ident(x)
+ case *ast.IfStmt:
+ tv.token(x.If, len("if"), semtok.TokKeyword, nil)
+ if x.Else != nil {
+ // x.Body.End() or x.Body.End()+1, not that it matters
+ pos := tv.findKeyword("else", x.Body.End(), x.Else.Pos())
+ tv.token(pos, len("else"), semtok.TokKeyword, nil)
+ }
+ case *ast.ImportSpec:
+ tv.importSpec(x)
+ pop()
+ return false
+ case *ast.IncDecStmt:
+ tv.token(x.TokPos, len(x.Tok.String()), semtok.TokOperator, nil)
+ case *ast.IndexExpr:
+ case *ast.IndexListExpr:
+ case *ast.InterfaceType:
+ tv.token(x.Interface, len("interface"), semtok.TokKeyword, nil)
+ case *ast.KeyValueExpr:
+ case *ast.LabeledStmt:
+ case *ast.MapType:
+ tv.token(x.Map, len("map"), semtok.TokKeyword, nil)
+ case *ast.ParenExpr:
+ case *ast.RangeStmt:
+ tv.token(x.For, len("for"), semtok.TokKeyword, nil)
+ // x.TokPos == token.NoPos is legal (for range foo {})
+ offset := x.TokPos
+ if offset == token.NoPos {
+ offset = x.For
+ }
+ pos := tv.findKeyword("range", offset, x.X.Pos())
+ tv.token(pos, len("range"), semtok.TokKeyword, nil)
+ case *ast.ReturnStmt:
+ tv.token(x.Return, len("return"), semtok.TokKeyword, nil)
+ case *ast.SelectStmt:
+ tv.token(x.Select, len("select"), semtok.TokKeyword, nil)
+ case *ast.SelectorExpr:
+ case *ast.SendStmt:
+ tv.token(x.Arrow, len("<-"), semtok.TokOperator, nil)
+ case *ast.SliceExpr:
+ case *ast.StarExpr:
+ tv.token(x.Star, len("*"), semtok.TokOperator, nil)
+ case *ast.StructType:
+ tv.token(x.Struct, len("struct"), semtok.TokKeyword, nil)
+ case *ast.SwitchStmt:
+ tv.token(x.Switch, len("switch"), semtok.TokKeyword, nil)
+ case *ast.TypeAssertExpr:
+ if x.Type == nil {
+ pos := tv.findKeyword("type", x.Lparen, x.Rparen)
+ tv.token(pos, len("type"), semtok.TokKeyword, nil)
+ }
+ case *ast.TypeSpec:
+ case *ast.TypeSwitchStmt:
+ tv.token(x.Switch, len("switch"), semtok.TokKeyword, nil)
+ case *ast.UnaryExpr:
+ tv.token(x.OpPos, len(x.Op.String()), semtok.TokOperator, nil)
+ case *ast.ValueSpec:
+ // things only seen with parsing or type errors, so ignore them
+ case *ast.BadDecl, *ast.BadExpr, *ast.BadStmt:
+ return true
+ // not going to see these
+ case *ast.File, *ast.Package:
+ tv.unexpected(fmt.Sprintf("implement %T %s", x, safetoken.Position(tv.pgf.Tok, x.Pos())))
+ // other things we knowingly ignore
+ case *ast.Comment, *ast.CommentGroup:
+ pop()
+ return false
+ default:
+ tv.unexpected(fmt.Sprintf("failed to implement %T", x))
+ }
+ return true
+}
+
+func (tv *tokenVisitor) ident(x *ast.Ident) {
+ if tv.ti == nil {
+ what, mods := tv.unkIdent(x)
+ if what != "" {
+ tv.token(x.Pos(), len(x.String()), what, mods)
+ }
+ if semDebug {
+ log.Printf(" nil %s/nil/nil %q %v %s", x.String(), what, mods, tv.strStack())
+ }
+ return
+ }
+ def := tv.ti.Defs[x]
+ if def != nil {
+ what, mods := tv.definitionFor(x, def)
+ if what != "" {
+ tv.token(x.Pos(), len(x.String()), what, mods)
+ }
+ if semDebug {
+ log.Printf(" for %s/%T/%T got %s %v (%s)", x.String(), def, def.Type(), what, mods, tv.strStack())
+ }
+ return
+ }
+ use := tv.ti.Uses[x]
+ tok := func(pos token.Pos, lng int, tok semtok.TokenType, mods []string) {
+ tv.token(pos, lng, tok, mods)
+ q := "nil"
+ if use != nil {
+ q = fmt.Sprintf("%T", use.Type())
+ }
+ if semDebug {
+ log.Printf(" use %s/%T/%s got %s %v (%s)", x.String(), use, q, tok, mods, tv.strStack())
+ }
+ }
+
+ switch y := use.(type) {
+ case nil:
+ what, mods := tv.unkIdent(x)
+ if what != "" {
+ tok(x.Pos(), len(x.String()), what, mods)
+ } else if semDebug {
+ // tok() wasn't called, so didn't log
+ log.Printf(" nil %s/%T/nil %q %v (%s)", x.String(), use, what, mods, tv.strStack())
+ }
+ return
+ case *types.Builtin:
+ tok(x.NamePos, len(x.Name), semtok.TokFunction, []string{"defaultLibrary"})
+ case *types.Const:
+ mods := []string{"readonly"}
+ tt := y.Type()
+ if _, ok := tt.(*types.Basic); ok {
+ tok(x.Pos(), len(x.String()), semtok.TokVariable, mods)
+ break
+ }
+ if ttx, ok := tt.(*types.Named); ok {
+ if x.String() == "iota" {
+ tv.unexpected(fmt.Sprintf("iota:%T", ttx))
+ }
+ if _, ok := ttx.Underlying().(*types.Basic); ok {
+ tok(x.Pos(), len(x.String()), semtok.TokVariable, mods)
+ break
+ }
+ tv.unexpected(fmt.Sprintf("%q/%T", x.String(), tt))
+ }
+ // can this happen? Don't think so
+ tv.unexpected(fmt.Sprintf("%s %T %#v", x.String(), tt, tt))
+ case *types.Func:
+ tok(x.Pos(), len(x.Name), semtok.TokFunction, nil)
+ case *types.Label:
+ // nothing to map it to
+ case *types.Nil:
+ // nil is a predeclared identifier
+ tok(x.Pos(), len("nil"), semtok.TokVariable, []string{"readonly", "defaultLibrary"})
+ case *types.PkgName:
+ tok(x.Pos(), len(x.Name), semtok.TokNamespace, nil)
+ case *types.TypeName: // could be a TokTypeParam
+ var mods []string
+ if _, ok := y.Type().(*types.Basic); ok {
+ mods = []string{"defaultLibrary"}
+ } else if _, ok := y.Type().(*types.TypeParam); ok {
+ tok(x.Pos(), len(x.String()), semtok.TokTypeParam, mods)
+ break
+ }
+ tok(x.Pos(), len(x.String()), semtok.TokType, mods)
+ case *types.Var:
+ if isSignature(y) {
+ tok(x.Pos(), len(x.Name), semtok.TokFunction, nil)
+ } else if tv.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), semtok.TokParameter, nil)
+ } else {
+ tok(x.Pos(), len(x.Name), semtok.TokVariable, nil)
+ }
+
+ default:
+ // can't happen
+ if use == nil {
+ msg := fmt.Sprintf("%#v %#v %#v", x, tv.ti.Defs[x], tv.ti.Uses[x])
+ tv.unexpected(msg)
+ }
+ if use.Type() != nil {
+ tv.unexpected(fmt.Sprintf("%s %T/%T,%#v", x.String(), use, use.Type(), use))
+ } else {
+ tv.unexpected(fmt.Sprintf("%s %T", x.String(), use))
+ }
+ }
+}
+
+func (tv *tokenVisitor) isParam(pos token.Pos) bool {
+ for i := len(tv.stack) - 1; i >= 0; i-- {
+ switch n := tv.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 _, ok := use.(*types.Var); !ok {
+ return false
+ }
+ v := use.Type()
+ if v == nil {
+ return false
+ }
+ if _, ok := v.(*types.Signature); ok {
+ return true
+ }
+ return false
+}
+
+// both tv.ti.Defs and tv.ti.Uses are nil. use the parse stack.
+// a lot of these only happen when the package doesn't compile
+// but in that case it is all best-effort from the parse tree
+func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) {
+ def := []string{"definition"}
+ n := len(tv.stack) - 2 // parent of Ident
+ if n < 0 {
+ tv.unexpected("no stack?")
+ return "", nil
+ }
+ switch nd := tv.stack[n].(type) {
+ case *ast.BinaryExpr, *ast.UnaryExpr, *ast.ParenExpr, *ast.StarExpr,
+ *ast.IncDecStmt, *ast.SliceExpr, *ast.ExprStmt, *ast.IndexExpr,
+ *ast.ReturnStmt, *ast.ChanType, *ast.SendStmt,
+ *ast.ForStmt, // possibly incomplete
+ *ast.IfStmt, /* condition */
+ *ast.KeyValueExpr, // either key or value
+ *ast.IndexListExpr:
+ return semtok.TokVariable, nil
+ case *ast.Ellipsis:
+ return semtok.TokType, nil
+ case *ast.CaseClause:
+ if n-2 >= 0 {
+ if _, ok := tv.stack[n-2].(*ast.TypeSwitchStmt); ok {
+ return semtok.TokType, nil
+ }
+ }
+ return semtok.TokVariable, nil
+ case *ast.ArrayType:
+ if x == nd.Len {
+ // or maybe a Type Param, but we can't just from the parse tree
+ return semtok.TokVariable, nil
+ } else {
+ return semtok.TokType, nil
+ }
+ case *ast.MapType:
+ return semtok.TokType, nil
+ case *ast.CallExpr:
+ if x == nd.Fun {
+ return semtok.TokFunction, nil
+ }
+ return semtok.TokVariable, nil
+ case *ast.SwitchStmt:
+ return semtok.TokVariable, nil
+ case *ast.TypeAssertExpr:
+ if x == nd.X {
+ return semtok.TokVariable, nil
+ } else if x == nd.Type {
+ return semtok.TokType, nil
+ }
+ case *ast.ValueSpec:
+ for _, p := range nd.Names {
+ if p == x {
+ return semtok.TokVariable, def
+ }
+ }
+ for _, p := range nd.Values {
+ if p == x {
+ return semtok.TokVariable, nil
+ }
+ }
+ return semtok.TokType, nil
+ case *ast.SelectorExpr: // e.ti.Selections[nd] is nil, so no help
+ if n-1 >= 0 {
+ if ce, ok := tv.stack[n-1].(*ast.CallExpr); ok {
+ // ... CallExpr SelectorExpr Ident (_.x())
+ if ce.Fun == nd && nd.Sel == x {
+ return semtok.TokFunction, nil
+ }
+ }
+ }
+ return semtok.TokVariable, nil
+ case *ast.AssignStmt:
+ for _, p := range nd.Lhs {
+ // x := ..., or x = ...
+ if p == x {
+ if nd.Tok != token.DEFINE {
+ def = nil
+ }
+ return semtok.TokVariable, def // '_' in _ = ...
+ }
+ }
+ // RHS, = x
+ return semtok.TokVariable, nil
+ case *ast.TypeSpec: // it's a type if it is either the Name or the Type
+ if x == nd.Type {
+ def = nil
+ }
+ return semtok.TokType, def
+ case *ast.Field:
+ // ident could be type in a field, or a method in an interface type, or a variable
+ if x == nd.Type {
+ return semtok.TokType, nil
+ }
+ if n-2 >= 0 {
+ _, okit := tv.stack[n-2].(*ast.InterfaceType)
+ _, okfl := tv.stack[n-1].(*ast.FieldList)
+ if okit && okfl {
+ return semtok.TokMethod, def
+ }
+ }
+ return semtok.TokVariable, nil
+ case *ast.LabeledStmt, *ast.BranchStmt:
+ // nothing to report
+ case *ast.CompositeLit:
+ if nd.Type == x {
+ return semtok.TokType, nil
+ }
+ return semtok.TokVariable, nil
+ case *ast.RangeStmt:
+ if nd.Tok != token.DEFINE {
+ def = nil
+ }
+ return semtok.TokVariable, def
+ case *ast.FuncDecl:
+ return semtok.TokFunction, def
+ default:
+ msg := fmt.Sprintf("%T undexpected: %s %s%q", nd, x.Name, tv.strStack(), tv.srcLine(x))
+ tv.unexpected(msg)
+ }
+ return "", nil
+}
+
+func isDeprecated(n *ast.CommentGroup) bool {
+ if n == nil {
+ return false
+ }
+ for _, c := range n.List {
+ if strings.HasPrefix(c.Text, "// Deprecated") {
+ return true
+ }
+ }
+ return false
+}
+
+func (tv *tokenVisitor) definitionFor(x *ast.Ident, def types.Object) (semtok.TokenType, []string) {
+ // PJW: def == types.Label? probably a nothing
+ // PJW: look into replacing these syntactic tests with types more generally
+ mods := []string{"definition"}
+ for i := len(tv.stack) - 1; i >= 0; i-- {
+ s := tv.stack[i]
+ switch y := s.(type) {
+ case *ast.AssignStmt, *ast.RangeStmt:
+ if x.Name == "_" {
+ return "", nil // not really a variable
+ }
+ return semtok.TokVariable, mods
+ case *ast.GenDecl:
+ if isDeprecated(y.Doc) {
+ mods = append(mods, "deprecated")
+ }
+ if y.Tok == token.CONST {
+ mods = append(mods, "readonly")
+ }
+ return semtok.TokVariable, mods
+ case *ast.FuncDecl:
+ // If x is immediately under a FuncDecl, it is a function or method
+ if i == len(tv.stack)-2 {
+ if isDeprecated(y.Doc) {
+ mods = append(mods, "deprecated")
+ }
+ if y.Recv != nil {
+ return semtok.TokMethod, mods
+ }
+ return semtok.TokFunction, mods
+ }
+ // if x < ... < FieldList < FuncDecl, this is the receiver, a variable
+ // PJW: maybe not. it might be a typeparameter in the type of the receiver
+ if _, ok := tv.stack[i+1].(*ast.FieldList); ok {
+ if _, ok := def.(*types.TypeName); ok {
+ return semtok.TokTypeParam, mods
+ }
+ return semtok.TokVariable, nil
+ }
+ // if x < ... < FieldList < FuncType < FuncDecl, this is a param
+ return semtok.TokParameter, mods
+ case *ast.FuncType: // is it in the TypeParams?
+ if isTypeParam(x, y) {
+ return semtok.TokTypeParam, mods
+ }
+ return semtok.TokParameter, mods
+ case *ast.InterfaceType:
+ return semtok.TokMethod, mods
+ case *ast.TypeSpec:
+ // GenDecl/Typespec/FuncType/FieldList/Field/Ident
+ // (type A func(b uint64)) (err error)
+ // b and err should not be semtok.TokType, but semtok.TokVariable
+ // and in GenDecl/TpeSpec/StructType/FieldList/Field/Ident
+ // (type A struct{b uint64}
+ // but on type B struct{C}), C is a type, but is not being defined.
+ // GenDecl/TypeSpec/FieldList/Field/Ident is a typeParam
+ if _, ok := tv.stack[i+1].(*ast.FieldList); ok {
+ return semtok.TokTypeParam, mods
+ }
+ fldm := tv.stack[len(tv.stack)-2]
+ if fld, ok := fldm.(*ast.Field); ok {
+ // if len(fld.names) == 0 this is a semtok.TokType, being used
+ if len(fld.Names) == 0 {
+ return semtok.TokType, nil
+ }
+ return semtok.TokVariable, mods
+ }
+ return semtok.TokType, mods
+ }
+ }
+ // can't happen
+ msg := fmt.Sprintf("failed to find the decl for %s", safetoken.Position(tv.pgf.Tok, x.Pos()))
+ tv.unexpected(msg)
+ return "", []string{""}
+}
+
+func isTypeParam(x *ast.Ident, y *ast.FuncType) bool {
+ tp := y.TypeParams
+ if tp == nil {
+ return false
+ }
+ for _, p := range tp.List {
+ for _, n := range p.Names {
+ if x == n {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (tv *tokenVisitor) multiline(start, end token.Pos, val string, tok semtok.TokenType) {
+ f := tv.fset.File(start)
+ // the hard part is finding the lengths of lines. include the \n
+ leng := func(line int) int {
+ n := f.LineStart(line)
+ if line >= f.LineCount() {
+ return f.Size() - int(n)
+ }
+ return int(f.LineStart(line+1) - n)
+ }
+ spos := safetoken.StartPosition(tv.fset, start)
+ epos := safetoken.EndPosition(tv.fset, end)
+ sline := spos.Line
+ eline := epos.Line
+ // first line is from spos.Column to end
+ tv.token(start, leng(sline)-spos.Column, tok, nil) // leng(sline)-1 - (spos.Column-1)
+ for i := sline + 1; i < eline; i++ {
+ // intermediate lines are from 1 to end
+ tv.token(f.LineStart(i), leng(i)-1, tok, nil) // avoid the newline
+ }
+ // last line is from 1 to epos.Column
+ tv.token(f.LineStart(eline), epos.Column-1, tok, nil) // columns are 1-based
+}
+
+// findKeyword finds a keyword rather than guessing its location
+func (tv *tokenVisitor) findKeyword(keyword string, start, end token.Pos) token.Pos {
+ offset := int(start) - tv.pgf.Tok.Base()
+ last := int(end) - tv.pgf.Tok.Base()
+ buf := tv.pgf.Src
+ idx := bytes.Index(buf[offset:last], []byte(keyword))
+ if idx != -1 {
+ return start + token.Pos(idx)
+ }
+ //(in unparsable programs: type _ <-<-chan int)
+ tv.unexpected(fmt.Sprintf("not found:%s %v", keyword, safetoken.StartPosition(tv.fset, start)))
+ return token.NoPos
+}
+
+func (tv *tokenVisitor) importSpec(d *ast.ImportSpec) {
+ // a local package name or the last component of the Path
+ if d.Name != nil {
+ nm := d.Name.String()
+ if nm != "_" && nm != "." {
+ tv.token(d.Name.Pos(), len(nm), semtok.TokNamespace, nil)
+ }
+ return // don't mark anything for . or _
+ }
+ importPath := metadata.UnquoteImportPath(d)
+ if importPath == "" {
+ return
+ }
+ // Import strings are implementation defined. Try to match with parse information.
+ depID := tv.pkg.Metadata().DepsByImpPath[importPath]
+ if depID == "" {
+ return
+ }
+ depMD := tv.metadataSource.Metadata(depID)
+ if depMD == nil {
+ // unexpected, but impact is that maybe some import is not colored
+ return
+ }
+ // Check whether the original literal contains the package's declared name.
+ j := strings.LastIndex(d.Path.Value, string(depMD.Name))
+ if j == -1 {
+ // Package name does not match import path, so there is nothing to report.
+ return
+ }
+ // Report virtual declaration at the position of the substring.
+ start := d.Path.Pos() + token.Pos(j)
+ tv.token(start, len(depMD.Name), semtok.TokNamespace, nil)
+}
+
+// log unexpected state
+func (tv *tokenVisitor) unexpected(msg string) {
+ if semDebug {
+ panic(msg)
+ }
+ event.Error(tv.ctx, tv.strStack(), errors.New(msg))
+}
+
+var godirectives = map[string]struct{}{
+ // https://pkg.go.dev/cmd/compile
+ "noescape": {},
+ "uintptrescapes": {},
+ "noinline": {},
+ "norace": {},
+ "nosplit": {},
+ "linkname": {},
+
+ // https://pkg.go.dev/go/build
+ "build": {},
+ "binary-only-package": {},
+ "embed": {},
+}
+
+// Tokenize godirective at the start of the comment c, if any, and the surrounding comment.
+// If there is any failure, emits the entire comment as a TokComment token.
+// Directives are highlighted as-is, even if used incorrectly. Typically there are
+// dedicated analyzers that will warn about misuse.
+func (tv *tokenVisitor) godirective(c *ast.Comment) {
+ // First check if '//go:directive args...' is a valid directive.
+ directive, args, _ := strings.Cut(c.Text, " ")
+ kind, _ := stringsCutPrefix(directive, "//go:")
+ if _, ok := godirectives[kind]; !ok {
+ // Unknown go: directive.
+ tv.token(c.Pos(), len(c.Text), semtok.TokComment, nil)
+ return
+ }
+
+ // Make the 'go:directive' part stand out, the rest is comments.
+ tv.token(c.Pos(), len("//"), semtok.TokComment, nil)
+
+ directiveStart := c.Pos() + token.Pos(len("//"))
+ tv.token(directiveStart, len(directive[len("//"):]), semtok.TokNamespace, nil)
+
+ if len(args) > 0 {
+ tailStart := c.Pos() + token.Pos(len(directive)+len(" "))
+ tv.token(tailStart, len(args), semtok.TokComment, nil)
+ }
+}
+
+// Go 1.20 strings.CutPrefix.
+func stringsCutPrefix(s, prefix string) (after string, found bool) {
+ if !strings.HasPrefix(s, prefix) {
+ return s, false
+ }
+ return s[len(prefix):], true
+}
diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/golang/signature_help.go
similarity index 95%
rename from gopls/internal/lsp/source/signature_help.go
rename to gopls/internal/golang/signature_help.go
index 4234bc4591a..0da1651571d 100644
--- a/gopls/internal/lsp/source/signature_help.go
+++ b/gopls/internal/golang/signature_help.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -13,16 +13,17 @@ import (
"strings"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/util/bug"
+ "golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/event"
)
func SignatureHelp(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.SignatureInformation, int, error) {
- ctx, done := event.Start(ctx, "source.SignatureHelp")
+ ctx, done := event.Start(ctx, "golang.SignatureHelp")
defer done()
// We need full type-checking here, as we must type-check function bodies in
@@ -78,7 +79,7 @@ FindCall:
}
// Inv: sig != nil
- qf := Qualifier(pgf.File, pkg.GetTypes(), info)
+ qf := typesutil.FileQualifier(pgf.File, pkg.GetTypes(), info)
// Get the object representing the function, if available.
// There is no object in certain cases such as calling a function returned by
diff --git a/gopls/internal/lsp/source/snapshot.go b/gopls/internal/golang/snapshot.go
similarity index 84%
rename from gopls/internal/lsp/source/snapshot.go
rename to gopls/internal/golang/snapshot.go
index 72e7d59d9a9..adcbfb9a811 100644
--- a/gopls/internal/lsp/source/snapshot.go
+++ b/gopls/internal/golang/snapshot.go
@@ -2,17 +2,16 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
"fmt"
- "golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// NarrowestMetadataForFile returns metadata for the narrowest package
@@ -89,16 +88,6 @@ func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri pro
return pkg, pgf, err
}
-// A FileSource maps URIs to FileHandles.
-type FileSource interface {
- // ReadFile returns the FileHandle for a given URI, either by
- // reading the content of the file or by obtaining it from a cache.
- //
- // Invariant: ReadFile must only return an error in the case of context
- // cancellation. If ctx.Err() is nil, the resulting error must also be nil.
- ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error)
-}
-
type ParsedGoFile = parsego.File
const (
diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/golang/stub.go
similarity index 84%
rename from gopls/internal/lsp/source/stub.go
rename to gopls/internal/golang/stub.go
index 138b7acb5ea..6a68ed294e9 100644
--- a/gopls/internal/lsp/source/stub.go
+++ b/gopls/internal/golang/stub.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"bytes"
@@ -13,15 +13,15 @@ import (
"go/token"
"go/types"
"io"
- "path"
+ pathpkg "path"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/analysis/stubmethods"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/diff"
@@ -56,23 +56,11 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.
return nil, nil, fmt.Errorf("file contains parse errors: %s", declPGF.URI)
}
- // Build import environment for the declaring file.
- importEnv := make(map[ImportPath]string) // value is local name
- for _, imp := range declPGF.File.Imports {
- importPath := metadata.UnquoteImportPath(imp)
- var name string
- if imp.Name != nil {
- name = imp.Name.Name
- if name == "_" {
- continue
- } else if name == "." {
- name = "" // see types.Qualifier
- }
- } else {
- // TODO(adonovan): may omit a vendor/ prefix; consult the Metadata.
- name = path.Base(string(importPath))
- }
- importEnv[importPath] = name // latest alias wins
+ // Find metadata for the concrete type's declaring package
+ // as we'll need its import mapping.
+ declMeta := findFileInDeps(snapshot, pkg.Metadata(), declPGF.URI)
+ if declMeta == nil {
+ return nil, nil, bug.Errorf("can't find metadata for file %s among dependencies of %s", declPGF.URI, pkg)
}
// Record all direct methods of the current object
@@ -134,10 +122,36 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.
return nil, nil, fmt.Errorf("no missing methods found")
}
+ // Build import environment for the declaring file.
+ // (typesutil.FileQualifier works only for complete
+ // import mappings, and requires types.)
+ importEnv := make(map[ImportPath]string) // value is local name
+ for _, imp := range declPGF.File.Imports {
+ importPath := metadata.UnquoteImportPath(imp)
+ var name string
+ if imp.Name != nil {
+ name = imp.Name.Name
+ if name == "_" {
+ continue
+ } else if name == "." {
+ name = "" // see types.Qualifier
+ }
+ } else {
+ // Use the correct name from the metadata of the imported
+ // package---not a guess based on the import path.
+ mp := snapshot.Metadata(declMeta.DepsByImpPath[importPath])
+ if mp == nil {
+ continue // can't happen?
+ }
+ name = string(mp.Name)
+ }
+ importEnv[importPath] = name // latest alias wins
+ }
+
// Create a package name qualifier that uses the
// locally appropriate imported package name.
// It records any needed new imports.
- // TODO(adonovan): factor with source.FormatVarType, stubmethods.RelativeToFiles?
+ // TODO(adonovan): factor with golang.FormatVarType?
//
// Prior to CL 469155 this logic preserved any renaming
// imports from the file that declares the interface
@@ -170,7 +184,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.
new := newImport{importPath: string(importPath)}
// For clarity, use a renaming import whenever the
// local name does not match the path's last segment.
- if name != path.Base(new.importPath) {
+ if name != pathpkg.Base(trimVersionSuffix(new.importPath)) {
new.name = name
}
newImports = append(newImports, new)
@@ -268,7 +282,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.
// Re-parse the file.
fset := token.NewFileSet()
- newF, err := parser.ParseFile(fset, declPGF.File.Name.Name, buf.Bytes(), parser.ParseComments)
+ newF, err := parser.ParseFile(fset, declPGF.URI.Path(), buf.Bytes(), parser.ParseComments)
if err != nil {
return nil, nil, fmt.Errorf("could not reparse file: %w", err)
}
@@ -303,3 +317,19 @@ func diffToTextEdits(tok *token.File, diffs []diff.Edit) []analysis.TextEdit {
}
return edits
}
+
+// trimVersionSuffix removes a trailing "/v2" (etc) suffix from a module path.
+//
+// This is only a heuristic as to the package's declared name, and
+// should only be used for stylistic decisions, such as whether it
+// would be clearer to use an explicit local name in the import
+// because the declared name differs from the result of this function.
+// When the name matters for correctness, look up the imported
+// package's Metadata.Name.
+func trimVersionSuffix(path string) string {
+ dir, base := pathpkg.Split(path)
+ if len(base) > 1 && base[0] == 'v' && strings.Trim(base[1:], "0123456789") == "" {
+ return dir // sans "/v2"
+ }
+ return path
+}
diff --git a/gopls/internal/lsp/source/symbols.go b/gopls/internal/golang/symbols.go
similarity index 97%
rename from gopls/internal/lsp/source/symbols.go
rename to gopls/internal/golang/symbols.go
index 676811881bc..390b8275183 100644
--- a/gopls/internal/lsp/source/symbols.go
+++ b/gopls/internal/golang/symbols.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -11,14 +11,14 @@ import (
"go/token"
"go/types"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
func DocumentSymbols(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentSymbol, error) {
- ctx, done := event.Start(ctx, "source.DocumentSymbols")
+ ctx, done := event.Start(ctx, "golang.DocumentSymbols")
defer done()
pgf, err := snapshot.ParseGo(ctx, fh, ParseFull)
diff --git a/gopls/internal/lsp/source/type_definition.go b/gopls/internal/golang/type_definition.go
similarity index 90%
rename from gopls/internal/lsp/source/type_definition.go
rename to gopls/internal/golang/type_definition.go
index c530344ca7f..306852cdcaf 100644
--- a/gopls/internal/lsp/source/type_definition.go
+++ b/gopls/internal/golang/type_definition.go
@@ -2,23 +2,23 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
"fmt"
"go/token"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/event"
)
// TypeDefinition handles the textDocument/typeDefinition request for Go files.
func TypeDefinition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) {
- ctx, done := event.Start(ctx, "source.TypeDefinition")
+ ctx, done := event.Start(ctx, "golang.TypeDefinition")
defer done()
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
diff --git a/gopls/internal/lsp/source/types_format.go b/gopls/internal/golang/types_format.go
similarity index 99%
rename from gopls/internal/lsp/source/types_format.go
rename to gopls/internal/golang/types_format.go
index b6306b98b8b..428e37f735b 100644
--- a/gopls/internal/lsp/source/types_format.go
+++ b/gopls/internal/golang/types_format.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"bytes"
@@ -15,8 +15,8 @@ import (
"go/types"
"strings"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/event"
diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/golang/util.go
similarity index 91%
rename from gopls/internal/lsp/source/util.go
rename to gopls/internal/golang/util.go
index 66a48566a9e..d6e71a964bb 100644
--- a/gopls/internal/lsp/source/util.go
+++ b/gopls/internal/golang/util.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -13,13 +13,12 @@ import (
"regexp"
"strings"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
- "golang.org/x/tools/gopls/internal/util/typesutil"
"golang.org/x/tools/internal/tokeninternal"
)
@@ -139,8 +138,6 @@ func Deref(typ types.Type) types.Type {
// findFileInDeps finds package metadata containing URI in the transitive
// dependencies of m. When using the Go command, the answer is unique.
-//
-// TODO(rfindley): refactor to share logic with findPackageInDeps?
func findFileInDeps(s metadata.Source, mp *metadata.Package, uri protocol.DocumentURI) *metadata.Package {
seen := make(map[PackageID]bool)
var search func(*metadata.Package) *metadata.Package
@@ -191,31 +188,6 @@ func CollectScopes(info *types.Info, path []ast.Node, pos token.Pos) []*types.Sc
return scopes
}
-// Qualifier returns a function that appropriately formats a types.PkgName
-// appearing in a *ast.File.
-func Qualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qualifier {
- // Construct mapping of import paths to their defined or implicit names.
- imports := make(map[*types.Package]string)
- for _, imp := range f.Imports {
- if pkgname, ok := typesutil.ImportedPkgName(info, imp); ok {
- imports[pkgname.Imported()] = pkgname.Name()
- }
- }
- // Define qualifier to replace full package paths with names of the imports.
- return func(p *types.Package) string {
- if p == pkg {
- return ""
- }
- if name, ok := imports[p]; ok {
- if name == "." {
- return ""
- }
- return name
- }
- return p.Name()
- }
-}
-
// requalifier returns a function that re-qualifies identifiers and qualified
// identifiers contained in targetFile using the given metadata qualifier.
func requalifier(s metadata.Source, targetFile *ast.File, targetMeta *metadata.Package, mq MetadataQualifier) func(string) string {
diff --git a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/golang/workspace_symbol.go
similarity index 98%
rename from gopls/internal/lsp/source/workspace_symbol.go
rename to gopls/internal/golang/workspace_symbol.go
index ccd65122922..4ab5a21b8a5 100644
--- a/gopls/internal/lsp/source/workspace_symbol.go
+++ b/gopls/internal/golang/workspace_symbol.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"context"
@@ -13,9 +13,9 @@ import (
"strings"
"unicode"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/fuzzy"
@@ -43,7 +43,7 @@ const maxSymbols = 100
// Session level configuration will define the SymbolMatcher to be used for the
// WorkspaceSymbols method.
func WorkspaceSymbols(ctx context.Context, matcher settings.SymbolMatcher, style settings.SymbolStyle, snapshots []*cache.Snapshot, query string) ([]protocol.SymbolInformation, error) {
- ctx, done := event.Start(ctx, "source.WorkspaceSymbols")
+ ctx, done := event.Start(ctx, "golang.WorkspaceSymbols")
defer done()
if query == "" {
return nil, nil
diff --git a/gopls/internal/lsp/source/workspace_symbol_test.go b/gopls/internal/golang/workspace_symbol_test.go
similarity index 98%
rename from gopls/internal/lsp/source/workspace_symbol_test.go
rename to gopls/internal/golang/workspace_symbol_test.go
index b102a2324ef..4982b767754 100644
--- a/gopls/internal/lsp/source/workspace_symbol_test.go
+++ b/gopls/internal/golang/workspace_symbol_test.go
@@ -2,12 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package source
+package golang
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/cache"
+ "golang.org/x/tools/gopls/internal/cache"
)
func TestParseQuery(t *testing.T) {
diff --git a/gopls/internal/hooks/analysis_119.go b/gopls/internal/hooks/analysis_119.go
index 34c6b938f0f..8fc7b461a73 100644
--- a/gopls/internal/hooks/analysis_119.go
+++ b/gopls/internal/hooks/analysis_119.go
@@ -1,62 +1,14 @@
-// Copyright 2019 The Go Authors. All rights reserved.
+// 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.19
-// +build go1.19
+//go:build !go1.20
+// +build !go1.20
package hooks
-import (
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/settings"
- "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"
-)
+import "golang.org/x/tools/gopls/internal/settings"
func updateAnalyzers(options *settings.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)
+ options.StaticcheckSupported = false
}
diff --git a/gopls/internal/hooks/analysis_120.go b/gopls/internal/hooks/analysis_120.go
new file mode 100644
index 00000000000..cded05eb4a6
--- /dev/null
+++ b/gopls/internal/hooks/analysis_120.go
@@ -0,0 +1,62 @@
+// 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.20
+// +build go1.20
+
+package hooks
+
+import (
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/settings"
+ "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 *settings.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/gofumpt_119.go
similarity index 64%
rename from gopls/internal/hooks/analysis_116.go
rename to gopls/internal/hooks/gofumpt_119.go
index 4ac32f35c66..d5bc98794f6 100644
--- a/gopls/internal/hooks/analysis_116.go
+++ b/gopls/internal/hooks/gofumpt_119.go
@@ -2,13 +2,12 @@
// 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
+//go:build !go1.20
+// +build !go1.20
package hooks
import "golang.org/x/tools/gopls/internal/settings"
-func updateAnalyzers(options *settings.Options) {
- options.StaticcheckSupported = false
+func updateGofumpt(options *settings.Options) {
}
diff --git a/gopls/internal/hooks/gofumpt_118.go b/gopls/internal/hooks/gofumpt_120.go
similarity index 98%
rename from gopls/internal/hooks/gofumpt_118.go
rename to gopls/internal/hooks/gofumpt_120.go
index a81444e1ef3..9ac2465efda 100644
--- a/gopls/internal/hooks/gofumpt_118.go
+++ b/gopls/internal/hooks/gofumpt_120.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 go1.18
-// +build go1.18
+//go:build go1.20
+// +build go1.20
package hooks
diff --git a/gopls/internal/hooks/gofumpt_118_test.go b/gopls/internal/hooks/gofumpt_120_test.go
similarity index 97%
rename from gopls/internal/hooks/gofumpt_118_test.go
rename to gopls/internal/hooks/gofumpt_120_test.go
index 838ce73176c..bb674980e1b 100644
--- a/gopls/internal/hooks/gofumpt_118_test.go
+++ b/gopls/internal/hooks/gofumpt_120_test.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 go1.18
-// +build go1.18
+//go:build go1.20
+// +build go1.20
package hooks
diff --git a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go
deleted file mode 100644
index 43df10e0237..00000000000
--- a/gopls/internal/lsp/cache/imports.go
+++ /dev/null
@@ -1,167 +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 cache
-
-import (
- "context"
- "fmt"
- "os"
- "reflect"
- "strings"
- "sync"
- "time"
-
- "golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/internal/event"
- "golang.org/x/tools/internal/event/keys"
- "golang.org/x/tools/internal/imports"
-)
-
-type importsState struct {
- ctx context.Context
-
- mu sync.Mutex
- processEnv *imports.ProcessEnv
- cacheRefreshDuration time.Duration
- cacheRefreshTimer *time.Timer
- cachedModFileHash file.Hash
- cachedBuildFlags []string
- cachedDirectoryFilters []string
-}
-
-func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot, fn func(context.Context, *imports.Options) error) error {
- ctx, done := event.Start(ctx, "cache.importsState.runProcessEnvFunc")
- defer done()
-
- s.mu.Lock()
- defer s.mu.Unlock()
-
- // 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.
- //
- // TODO(rfindley): consider instead hashing on-disk modfiles here.
- var modFileHash file.Hash
- for m := range snapshot.view.workspaceModFiles {
- fh, err := snapshot.ReadFile(ctx, m)
- if err != nil {
- return err
- }
- modFileHash.XORWith(fh.Identity().Hash)
- }
-
- // view.goEnv is immutable -- changes make a new view. Options can change.
- // We can't compare build flags directly because we may add -modfile.
- localPrefix := snapshot.Options().Local
- currentBuildFlags := snapshot.Options().BuildFlags
- currentDirectoryFilters := snapshot.Options().DirectoryFilters
- changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) ||
- snapshot.Options().VerboseOutput != (s.processEnv.Logf != nil) ||
- modFileHash != s.cachedModFileHash ||
- !reflect.DeepEqual(snapshot.Options().DirectoryFilters, s.cachedDirectoryFilters)
-
- // If anything relevant to imports has changed, clear caches and
- // update the processEnv. Clearing caches blocks on any background
- // scans.
- if changed {
- if err := populateProcessEnvFromSnapshot(ctx, s.processEnv, snapshot); err != nil {
- return err
- }
-
- if resolver, err := s.processEnv.GetResolver(); err == nil {
- if modResolver, ok := resolver.(*imports.ModuleResolver); ok {
- modResolver.ClearForNewMod()
- }
- }
-
- s.cachedModFileHash = modFileHash
- s.cachedBuildFlags = currentBuildFlags
- s.cachedDirectoryFilters = currentDirectoryFilters
- }
-
- // Run the user function.
- opts := &imports.Options{
- // Defaults.
- AllErrors: true,
- Comments: true,
- Fragment: true,
- FormatOnly: false,
- TabIndent: true,
- TabWidth: 8,
- Env: s.processEnv,
- LocalPrefix: localPrefix,
- }
-
- if err := fn(ctx, opts); err != nil {
- return err
- }
-
- if s.cacheRefreshTimer == nil {
- // Don't refresh more than twice per minute.
- delay := 30 * time.Second
- // Don't spend more than a couple percent of the time refreshing.
- if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay {
- delay = adaptive
- }
- s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv)
- }
-
- return nil
-}
-
-// populateProcessEnvFromSnapshot sets the dynamically configurable fields for
-// the view's process environment. Assumes that the caller is holding the
-// importsState mutex.
-func populateProcessEnvFromSnapshot(ctx context.Context, pe *imports.ProcessEnv, snapshot *Snapshot) error {
- ctx, done := event.Start(ctx, "cache.populateProcessEnvFromSnapshot")
- defer done()
-
- if snapshot.Options().VerboseOutput {
- pe.Logf = func(format string, args ...interface{}) {
- event.Log(ctx, fmt.Sprintf(format, args...))
- }
- } else {
- pe.Logf = nil
- }
-
- pe.WorkingDir = snapshot.view.root.Path()
- pe.ModFlag = "readonly" // processEnv operations should not mutate the modfile
- pe.Env = map[string]string{}
- pe.BuildFlags = append([]string{}, snapshot.Options().BuildFlags...)
- env := append(append(os.Environ(), snapshot.Options().EnvSlice()...), "GO111MODULE="+snapshot.view.adjustedGO111MODULE())
- for _, kv := range env {
- split := strings.SplitN(kv, "=", 2)
- if len(split) != 2 {
- continue
- }
- pe.Env[split[0]] = split[1]
- }
- return nil
-}
-
-func (s *importsState) refreshProcessEnv() {
- ctx, done := event.Start(s.ctx, "cache.importsState.refreshProcessEnv")
- defer done()
-
- start := time.Now()
-
- s.mu.Lock()
- env := s.processEnv
- if resolver, err := s.processEnv.GetResolver(); err == nil {
- resolver.ClearForNewScan()
- }
- s.mu.Unlock()
-
- event.Log(s.ctx, "background imports cache refresh starting")
- if err := imports.PrimeCache(context.Background(), env); err == nil {
- event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)))
- } else {
- event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err))
- }
- s.mu.Lock()
- s.cacheRefreshDuration = time.Since(start)
- s.cacheRefreshTimer = nil
- s.mu.Unlock()
-}
diff --git a/gopls/internal/lsp/lsprpc/commandinterceptor.go b/gopls/internal/lsp/lsprpc/commandinterceptor.go
deleted file mode 100644
index 607ee9c9e9f..00000000000
--- a/gopls/internal/lsp/lsprpc/commandinterceptor.go
+++ /dev/null
@@ -1,44 +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 lsprpc
-
-import (
- "context"
- "encoding/json"
-
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2"
-)
-
-// HandlerMiddleware is a middleware that only modifies the jsonrpc2 handler.
-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 {
- opts := binder.Bind(ctx, conn)
- opts.Handler = hmw(opts.Handler)
- return opts
- })
- })
-}
-
-func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) (interface{}, error)) Middleware {
- return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler {
- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
- if req.Method == "workspace/executeCommand" {
- var params protocol.ExecuteCommandParams
- if err := json.Unmarshal(req.Params, ¶ms); err == nil {
- if params.Command == command {
- return run(¶ms)
- }
- }
- }
-
- return delegate.Handle(ctx, req)
- })
- })
-}
diff --git a/gopls/internal/lsp/lsprpc/goenv_test.go b/gopls/internal/lsp/lsprpc/goenv_test.go
deleted file mode 100644
index 3030ef34dfc..00000000000
--- a/gopls/internal/lsp/lsprpc/goenv_test.go
+++ /dev/null
@@ -1,68 +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 lsprpc_test
-
-import (
- "context"
- "testing"
-
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/internal/testenv"
-
- . "golang.org/x/tools/gopls/internal/lsp/lsprpc"
-)
-
-type initServer struct {
- protocol.Server
-
- params *protocol.ParamInitialize
-}
-
-func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
- s.params = params
- return &protocol.InitializeResult{}, nil
-}
-
-func TestGoEnvMiddleware(t *testing.T) {
- testenv.NeedsTool(t, "go")
-
- ctx := context.Background()
-
- server := &initServer{}
- env := new(TestEnv)
- defer env.Shutdown(t)
- l, _ := env.serve(ctx, t, staticServerBinder(server))
- mw, err := GoEnvMiddleware()
- if err != nil {
- t.Fatal(err)
- }
- binder := mw(NewForwardBinder(l.Dialer()))
- l, _ = env.serve(ctx, t, binder)
- conn := env.dial(ctx, t, l.Dialer(), noopBinder, true)
- dispatch := protocol.ServerDispatcherV2(conn)
- initParams := &protocol.ParamInitialize{}
- initParams.InitializationOptions = map[string]interface{}{
- "env": map[string]interface{}{
- "GONOPROXY": "example.com",
- },
- }
- if _, err := dispatch.Initialize(ctx, initParams); err != nil {
- t.Fatal(err)
- }
-
- if server.params == nil {
- t.Fatalf("initialize params are unset")
- }
- envOpts := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{})
-
- // Check for an arbitrary Go variable. It should be set.
- if _, ok := envOpts["GOPRIVATE"]; !ok {
- t.Errorf("Go environment variable GOPRIVATE unset in initialization options")
- }
- // Check that the variable present in our user config was not overwritten.
- if got, want := envOpts["GONOPROXY"], "example.com"; got != want {
- t.Errorf("GONOPROXY=%q, want %q", got, want)
- }
-}
diff --git a/gopls/internal/lsp/lsprpc/middleware_test.go b/gopls/internal/lsp/lsprpc/middleware_test.go
deleted file mode 100644
index c528eae5c62..00000000000
--- a/gopls/internal/lsp/lsprpc/middleware_test.go
+++ /dev/null
@@ -1,93 +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 lsprpc_test
-
-import (
- "context"
- "errors"
- "fmt"
- "testing"
- "time"
-
- . "golang.org/x/tools/gopls/internal/lsp/lsprpc"
- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2"
-)
-
-var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions {
- return jsonrpc2_v2.ConnectionOptions{}
-})
-
-func TestHandshakeMiddleware(t *testing.T) {
- sh := &Handshaker{
- Metadata: Metadata{
- "answer": 42,
- },
- }
- ctx := context.Background()
- env := new(TestEnv)
- defer env.Shutdown(t)
- l, _ := env.serve(ctx, t, sh.Middleware(noopBinder))
- conn := env.dial(ctx, t, l.Dialer(), noopBinder, false)
- ch := &Handshaker{
- Metadata: Metadata{
- "question": 6 * 9,
- },
- }
-
- check := func(connected bool) error {
- clients := sh.Peers()
- servers := ch.Peers()
- want := 0
- if connected {
- want = 1
- }
- if got := len(clients); got != want {
- return fmt.Errorf("got %d clients on the server, want %d", got, want)
- }
- if got := len(servers); got != want {
- return fmt.Errorf("got %d servers on the client, want %d", got, want)
- }
- if !connected {
- return nil
- }
- client := clients[0]
- server := servers[0]
- if _, ok := client.Metadata["question"]; !ok {
- return errors.New("no client metadata")
- }
- if _, ok := server.Metadata["answer"]; !ok {
- return errors.New("no server metadata")
- }
- if client.LocalID != server.RemoteID {
- return fmt.Errorf("client.LocalID == %d, server.PeerID == %d", client.LocalID, server.RemoteID)
- }
- if client.RemoteID != server.LocalID {
- return fmt.Errorf("client.PeerID == %d, server.LocalID == %d", client.RemoteID, server.LocalID)
- }
- return nil
- }
-
- if err := check(false); err != nil {
- t.Fatalf("before handshake: %v", err)
- }
- ch.ClientHandshake(ctx, conn)
- if err := check(true); err != nil {
- t.Fatalf("after handshake: %v", err)
- }
- conn.Close()
- // Wait for up to ~2s for connections to get cleaned up.
- delay := 25 * time.Millisecond
- for retries := 3; retries >= 0; retries-- {
- time.Sleep(delay)
- err := check(false)
- if err == nil {
- return
- }
- if retries == 0 {
- t.Fatalf("after closing connection: %v", err)
- }
- delay *= 4
- }
-}
diff --git a/gopls/internal/lsp/lsprpc/autostart_default.go b/gopls/internal/lsprpc/autostart_default.go
similarity index 100%
rename from gopls/internal/lsp/lsprpc/autostart_default.go
rename to gopls/internal/lsprpc/autostart_default.go
diff --git a/gopls/internal/lsp/lsprpc/autostart_posix.go b/gopls/internal/lsprpc/autostart_posix.go
similarity index 100%
rename from gopls/internal/lsp/lsprpc/autostart_posix.go
rename to gopls/internal/lsprpc/autostart_posix.go
diff --git a/gopls/internal/hooks/gofumpt_117.go b/gopls/internal/lsprpc/binder.go
similarity index 50%
rename from gopls/internal/hooks/gofumpt_117.go
rename to gopls/internal/lsprpc/binder.go
index 71886357704..708e0ad6afe 100644
--- a/gopls/internal/hooks/gofumpt_117.go
+++ b/gopls/internal/lsprpc/binder.go
@@ -2,12 +2,4 @@
// 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) {
-}
+package lsprpc
diff --git a/gopls/internal/lsp/lsprpc/binder_test.go b/gopls/internal/lsprpc/binder_test.go
similarity index 67%
rename from gopls/internal/lsp/lsprpc/binder_test.go
rename to gopls/internal/lsprpc/binder_test.go
index 3315c3eb775..042056e7777 100644
--- a/gopls/internal/lsp/lsprpc/binder_test.go
+++ b/gopls/internal/lsprpc/binder_test.go
@@ -11,12 +11,64 @@ import (
"testing"
"time"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2"
- . "golang.org/x/tools/gopls/internal/lsp/lsprpc"
+ . "golang.org/x/tools/gopls/internal/lsprpc"
)
+// ServerBinder binds incoming connections to a new server.
+type ServerBinder struct {
+ newServer ServerFunc
+}
+
+func NewServerBinder(newServer ServerFunc) *ServerBinder {
+ return &ServerBinder{newServer: newServer}
+}
+
+// streamServer used to have this method, but it was never used.
+// TODO(adonovan): figure out whether we need any of this machinery
+// and, if not, delete it. In the meantime, it's better that it sit
+// in the test package with all the other mothballed machinery
+// than in the production code where it would couple streamServer
+// and ServerBinder.
+/*
+func (s *streamServer) Binder() *ServerBinder {
+ newServer := func(ctx context.Context, client protocol.ClientCloser) protocol.Server {
+ session := cache.NewSession(ctx, s.cache)
+ svr := s.serverForTest
+ if svr == nil {
+ options := settings.DefaultOptions(s.optionsOverrides)
+ svr = server.New(session, client, options)
+ if instance := debug.GetInstance(ctx); instance != nil {
+ instance.AddService(svr, session)
+ }
+ }
+ return svr
+ }
+ return NewServerBinder(newServer)
+}
+*/
+
+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)
+ // Wrap the server handler to inject the client into each request context, so
+ // that log events are reflected back to the client.
+ wrapped := jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
+ ctx = protocol.WithClient(ctx, client)
+ return serverHandler.Handle(ctx, req)
+ })
+ preempter := &Canceler{
+ Conn: conn,
+ }
+ return jsonrpc2_v2.ConnectionOptions{
+ Handler: wrapped,
+ Preempter: preempter,
+ }
+}
+
type TestEnv struct {
Conns []*jsonrpc2_v2.Connection
Servers []*jsonrpc2_v2.Server
diff --git a/gopls/internal/lsp/lsprpc/commandinterceptor_test.go b/gopls/internal/lsprpc/commandinterceptor_test.go
similarity index 55%
rename from gopls/internal/lsp/lsprpc/commandinterceptor_test.go
rename to gopls/internal/lsprpc/commandinterceptor_test.go
index 555f15130cc..7c83ef993f0 100644
--- a/gopls/internal/lsp/lsprpc/commandinterceptor_test.go
+++ b/gopls/internal/lsprpc/commandinterceptor_test.go
@@ -6,13 +6,32 @@ package lsprpc_test
import (
"context"
+ "encoding/json"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2"
- . "golang.org/x/tools/gopls/internal/lsp/lsprpc"
+ . "golang.org/x/tools/gopls/internal/lsprpc"
)
+func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) (interface{}, error)) Middleware {
+ return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler {
+ return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
+ if req.Method == "workspace/executeCommand" {
+ var params protocol.ExecuteCommandParams
+ if err := json.Unmarshal(req.Params, ¶ms); err == nil {
+ if params.Command == command {
+ return run(¶ms)
+ }
+ }
+ }
+
+ return delegate.Handle(ctx, req)
+ })
+ })
+}
+
func TestCommandInterceptor(t *testing.T) {
const command = "foo"
caught := false
diff --git a/gopls/internal/lsp/lsprpc/dialer.go b/gopls/internal/lsprpc/dialer.go
similarity index 87%
rename from gopls/internal/lsp/lsprpc/dialer.go
rename to gopls/internal/lsprpc/dialer.go
index e962c60579c..a5f038df9f1 100644
--- a/gopls/internal/lsp/lsprpc/dialer.go
+++ b/gopls/internal/lsprpc/dialer.go
@@ -16,12 +16,12 @@ import (
"golang.org/x/tools/internal/event"
)
-// AutoNetwork is the pseudo network type used to signal that gopls should use
+// autoNetwork is the pseudo network type used to signal that gopls should use
// automatic discovery to resolve a remote address.
-const AutoNetwork = "auto"
+const autoNetwork = "auto"
-// An AutoDialer is a jsonrpc2 dialer that understands the 'auto' network.
-type AutoDialer struct {
+// An autoDialer is a jsonrpc2 dialer that understands the 'auto' network.
+type autoDialer struct {
network, addr string // the 'real' network and address
isAuto bool // whether the server is on the 'auto' network
@@ -29,12 +29,12 @@ type AutoDialer struct {
argFunc func(network, addr string) []string
}
-func NewAutoDialer(rawAddr string, argFunc func(network, addr string) []string) (*AutoDialer, error) {
- d := AutoDialer{
+func newAutoDialer(rawAddr string, argFunc func(network, addr string) []string) (*autoDialer, error) {
+ d := autoDialer{
argFunc: argFunc,
}
d.network, d.addr = ParseAddr(rawAddr)
- if d.network == AutoNetwork {
+ if d.network == autoNetwork {
d.isAuto = true
bin, err := os.Executable()
if err != nil {
@@ -47,14 +47,14 @@ func NewAutoDialer(rawAddr string, argFunc func(network, addr string) []string)
}
// Dial implements the jsonrpc2.Dialer interface.
-func (d *AutoDialer) Dial(ctx context.Context) (io.ReadWriteCloser, error) {
+func (d *autoDialer) Dial(ctx context.Context) (io.ReadWriteCloser, error) {
conn, err := d.dialNet(ctx)
return conn, err
}
// TODO(rFindley): remove this once we no longer need to integrate with v1 of
// the jsonrpc2 package.
-func (d *AutoDialer) dialNet(ctx context.Context) (net.Conn, error) {
+func (d *autoDialer) dialNet(ctx context.Context) (net.Conn, error) {
// Attempt to verify that we own the remote. This is imperfect, but if we can
// determine that the remote is owned by a different user, we should fail.
ok, err := verifyRemoteOwnership(d.network, d.addr)
diff --git a/gopls/internal/lsp/lsprpc/binder.go b/gopls/internal/lsprpc/export_test.go
similarity index 68%
rename from gopls/internal/lsp/lsprpc/binder.go
rename to gopls/internal/lsprpc/export_test.go
index 01e59f7bb62..509129870dc 100644
--- a/gopls/internal/lsp/lsprpc/binder.go
+++ b/gopls/internal/lsprpc/export_test.go
@@ -1,68 +1,32 @@
-// Copyright 2021 The Go Authors. All rights reserved.
+// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package lsprpc
+// This file defines things (and opens backdoors) needed only by tests.
+
import (
"context"
"encoding/json"
"fmt"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2"
"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
-
-func (f BinderFunc) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions {
- return f(ctx, conn)
-}
-
-// Middleware defines a transformation of jsonrpc2 Binders, that may be
-// composed to build jsonrpc2 servers.
-type Middleware func(jsonrpc2_v2.Binder) jsonrpc2_v2.Binder
+const HandshakeMethod = handshakeMethod
// A ServerFunc is used to construct an LSP server for a given client.
type ServerFunc func(context.Context, protocol.ClientCloser) protocol.Server
-// ServerBinder binds incoming connections to a new server.
-type ServerBinder struct {
- newServer ServerFunc
+type Canceler struct {
+ Conn *jsonrpc2_v2.Connection
}
-func NewServerBinder(newServer ServerFunc) *ServerBinder {
- return &ServerBinder{newServer: newServer}
-}
-
-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)
- // Wrap the server handler to inject the client into each request context, so
- // that log events are reflected back to the client.
- wrapped := jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
- ctx = protocol.WithClient(ctx, client)
- return serverHandler.Handle(ctx, req)
- })
- preempter := &canceler{
- conn: conn,
- }
- return jsonrpc2_v2.ConnectionOptions{
- Handler: wrapped,
- Preempter: preempter,
- }
-}
-
-type canceler struct {
- conn *jsonrpc2_v2.Connection
-}
-
-func (c *canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
+func (c *Canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
if req.Method != "$/cancelRequest" {
return nil, jsonrpc2_v2.ErrNotHandled
}
@@ -79,7 +43,7 @@ func (c *canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (inter
default:
return nil, fmt.Errorf("%w: invalid ID type %T", jsonrpc2_v2.ErrParse, params.ID)
}
- c.conn.Cancel(id)
+ c.Conn.Cancel(id)
return nil, nil
}
@@ -111,8 +75,8 @@ func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection)
b.onBind(serverConn)
}
server := protocol.ServerDispatcherV2(serverConn)
- preempter := &canceler{
- conn: conn,
+ preempter := &Canceler{
+ Conn: conn,
}
detached := xcontext.Detach(ctx)
go func() {
@@ -127,22 +91,52 @@ func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection)
}
}
+func NewClientBinder(newClient ClientFunc) *clientBinder {
+ return &clientBinder{newClient}
+}
+
// A ClientFunc is used to construct an LSP client for a given server.
type ClientFunc func(context.Context, protocol.Server) protocol.Client
-// ClientBinder binds an LSP client to an incoming connection.
-type ClientBinder struct {
+// clientBinder binds an LSP client to an incoming connection.
+type clientBinder struct {
newClient ClientFunc
}
-func NewClientBinder(newClient ClientFunc) *ClientBinder {
- return &ClientBinder{newClient}
-}
-
-func (b *ClientBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions {
+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),
}
}
+
+// HandlerMiddleware is a middleware that only modifies the jsonrpc2 handler.
+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 {
+ opts := binder.Bind(ctx, conn)
+ opts.Handler = hmw(opts.Handler)
+ return opts
+ })
+ })
+}
+
+// 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
+
+func (f BinderFunc) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions {
+ return f(ctx, conn)
+}
+
+// Middleware defines a transformation of jsonrpc2 Binders, that may be
+// composed to build jsonrpc2 servers.
+type Middleware func(jsonrpc2_v2.Binder) jsonrpc2_v2.Binder
+
+var GetGoEnv = getGoEnv
+
+type StreamServer = streamServer
diff --git a/gopls/internal/lsprpc/goenv.go b/gopls/internal/lsprpc/goenv.go
new file mode 100644
index 00000000000..52ec08ff7eb
--- /dev/null
+++ b/gopls/internal/lsprpc/goenv.go
@@ -0,0 +1,34 @@
+// 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 lsprpc
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "golang.org/x/tools/internal/gocommand"
+)
+
+func getGoEnv(ctx context.Context, env map[string]interface{}) (map[string]string, error) {
+ var runEnv []string
+ for k, v := range env {
+ runEnv = append(runEnv, fmt.Sprintf("%s=%s", k, v))
+ }
+ runner := gocommand.Runner{}
+ output, err := runner.Run(ctx, gocommand.Invocation{
+ Verb: "env",
+ Args: []string{"-json"},
+ Env: runEnv,
+ })
+ if err != nil {
+ return nil, err
+ }
+ envmap := make(map[string]string)
+ if err := json.Unmarshal(output.Bytes(), &envmap); err != nil {
+ return nil, err
+ }
+ return envmap, nil
+}
diff --git a/gopls/internal/lsp/lsprpc/goenv.go b/gopls/internal/lsprpc/goenv_test.go
similarity index 52%
rename from gopls/internal/lsp/lsprpc/goenv.go
rename to gopls/internal/lsprpc/goenv_test.go
index b7717844f17..6c41540fafb 100644
--- a/gopls/internal/lsp/lsprpc/goenv.go
+++ b/gopls/internal/lsprpc/goenv_test.go
@@ -2,18 +2,21 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package lsprpc
+package lsprpc_test
import (
"context"
"encoding/json"
"fmt"
"os"
+ "testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"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/testenv"
+
+ . "golang.org/x/tools/gopls/internal/lsprpc"
)
func GoEnvMiddleware() (Middleware, error) {
@@ -29,6 +32,8 @@ func GoEnvMiddleware() (Middleware, error) {
}), nil
}
+// This function is almost identical to addGoEnvToInitializeRequest in lsprpc.go.
+// Make changes in parallel.
func addGoEnvToInitializeRequestV2(ctx context.Context, req *jsonrpc2_v2.Request) error {
var params protocol.ParamInitialize
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
@@ -51,7 +56,7 @@ func addGoEnvToInitializeRequestV2(ctx context.Context, req *jsonrpc2_v2.Request
if !ok {
return fmt.Errorf("env option is %T, expected a map", envOpt)
}
- goenv, err := getGoEnv(ctx, env)
+ goenv, err := GetGoEnv(ctx, env)
if err != nil {
return err
}
@@ -74,23 +79,55 @@ func addGoEnvToInitializeRequestV2(ctx context.Context, req *jsonrpc2_v2.Request
return nil
}
-func getGoEnv(ctx context.Context, env map[string]interface{}) (map[string]string, error) {
- var runEnv []string
- for k, v := range env {
- runEnv = append(runEnv, fmt.Sprintf("%s=%s", k, v))
- }
- runner := gocommand.Runner{}
- output, err := runner.Run(ctx, gocommand.Invocation{
- Verb: "env",
- Args: []string{"-json"},
- Env: runEnv,
- })
+type initServer struct {
+ protocol.Server
+
+ params *protocol.ParamInitialize
+}
+
+func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
+ s.params = params
+ return &protocol.InitializeResult{}, nil
+}
+
+func TestGoEnvMiddleware(t *testing.T) {
+ testenv.NeedsTool(t, "go")
+
+ ctx := context.Background()
+
+ server := &initServer{}
+ env := new(TestEnv)
+ defer env.Shutdown(t)
+ l, _ := env.serve(ctx, t, staticServerBinder(server))
+ mw, err := GoEnvMiddleware()
if err != nil {
- return nil, err
+ t.Fatal(err)
+ }
+ binder := mw(NewForwardBinder(l.Dialer()))
+ l, _ = env.serve(ctx, t, binder)
+ conn := env.dial(ctx, t, l.Dialer(), noopBinder, true)
+ dispatch := protocol.ServerDispatcherV2(conn)
+ initParams := &protocol.ParamInitialize{}
+ initParams.InitializationOptions = map[string]interface{}{
+ "env": map[string]interface{}{
+ "GONOPROXY": "example.com",
+ },
+ }
+ if _, err := dispatch.Initialize(ctx, initParams); err != nil {
+ t.Fatal(err)
+ }
+
+ if server.params == nil {
+ t.Fatalf("initialize params are unset")
+ }
+ envOpts := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{})
+
+ // Check for an arbitrary Go variable. It should be set.
+ if _, ok := envOpts["GOPRIVATE"]; !ok {
+ t.Errorf("Go environment variable GOPRIVATE unset in initialization options")
}
- envmap := make(map[string]string)
- if err := json.Unmarshal(output.Bytes(), &envmap); err != nil {
- return nil, err
+ // Check that the variable present in our user config was not overwritten.
+ if got, want := envOpts["GONOPROXY"], "example.com"; got != want {
+ t.Errorf("GONOPROXY=%q, want %q", got, want)
}
- return envmap, nil
}
diff --git a/gopls/internal/lsp/lsprpc/lsprpc.go b/gopls/internal/lsprpc/lsprpc.go
similarity index 88%
rename from gopls/internal/lsp/lsprpc/lsprpc.go
rename to gopls/internal/lsprpc/lsprpc.go
index 006c98e9e95..0497612106d 100644
--- a/gopls/internal/lsp/lsprpc/lsprpc.go
+++ b/gopls/internal/lsprpc/lsprpc.go
@@ -19,10 +19,10 @@ import (
"sync/atomic"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug"
- "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/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/server"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/event"
@@ -33,9 +33,9 @@ import (
// Unique identifiers for client/server.
var serverIndex int64
-// The StreamServer type is a jsonrpc2.StreamServer that handles incoming
+// The streamServer type is a jsonrpc2.streamServer that handles incoming
// streams as a new LSP session, using a shared cache.
-type StreamServer struct {
+type streamServer struct {
cache *cache.Cache
// daemon controls whether or not to log new connections.
daemon bool
@@ -50,29 +50,13 @@ type StreamServer struct {
// NewStreamServer creates a StreamServer using the shared cache. If
// withTelemetry is true, each session is instrumented with telemetry that
// records RPC statistics.
-func NewStreamServer(cache *cache.Cache, daemon bool, optionsFunc func(*settings.Options)) *StreamServer {
- return &StreamServer{cache: cache, daemon: daemon, optionsOverrides: optionsFunc}
-}
-
-func (s *StreamServer) Binder() *ServerBinder {
- newServer := func(ctx context.Context, client protocol.ClientCloser) protocol.Server {
- session := cache.NewSession(ctx, s.cache)
- svr := s.serverForTest
- if svr == nil {
- options := settings.DefaultOptions(s.optionsOverrides)
- svr = server.New(session, client, options)
- if instance := debug.GetInstance(ctx); instance != nil {
- instance.AddService(svr, session)
- }
- }
- return svr
- }
- return NewServerBinder(newServer)
+func NewStreamServer(cache *cache.Cache, daemon bool, optionsFunc func(*settings.Options)) jsonrpc2.StreamServer {
+ return &streamServer{cache: cache, daemon: daemon, optionsOverrides: optionsFunc}
}
// ServeStream implements the jsonrpc2.StreamServer interface, by handling
// incoming streams using a new lsp server.
-func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error {
+func (s *streamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error {
client := protocol.ClientDispatcher(conn)
session := cache.NewSession(ctx, s.cache)
svr := s.serverForTest
@@ -110,14 +94,14 @@ func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) erro
return conn.Err()
}
-// A Forwarder is a jsonrpc2.StreamServer that handles an LSP stream by
+// A forwarder is a jsonrpc2.StreamServer that handles an LSP stream by
// forwarding it to a remote. This is used when the gopls process started by
// the editor is in the `-remote` mode, which means it finds and connects to a
// separate gopls daemon. In these cases, we still want the forwarder gopls to
// be instrumented with telemetry, and want to be able to in some cases hijack
// the jsonrpc2 connection with the daemon.
-type Forwarder struct {
- dialer *AutoDialer
+type forwarder struct {
+ dialer *autoDialer
mu sync.Mutex
// Hold on to the server connection so that we can redo the handshake if any
@@ -126,28 +110,29 @@ type Forwarder struct {
serverID string
}
-// NewForwarder creates a new Forwarder, ready to forward connections to the
+// NewForwarder creates a new forwarder (a [jsonrpc2.StreamServer]),
+// ready to forward connections to the
// remote server specified by rawAddr. If provided and rawAddr indicates an
// 'automatic' address (starting with 'auto;'), argFunc may be used to start a
// remote server for the auto-discovered address.
-func NewForwarder(rawAddr string, argFunc func(network, address string) []string) (*Forwarder, error) {
- dialer, err := NewAutoDialer(rawAddr, argFunc)
+func NewForwarder(rawAddr string, argFunc func(network, address string) []string) (jsonrpc2.StreamServer, error) {
+ dialer, err := newAutoDialer(rawAddr, argFunc)
if err != nil {
return nil, err
}
- fwd := &Forwarder{
+ fwd := &forwarder{
dialer: dialer,
}
return fwd, nil
}
-// QueryServerState queries the server state of the current server.
-func QueryServerState(ctx context.Context, addr string) (*ServerState, error) {
+// QueryServerState returns a JSON-encodable struct describing the state of the named server.
+func QueryServerState(ctx context.Context, addr string) (any, error) {
serverConn, err := dialRemote(ctx, addr)
if err != nil {
return nil, err
}
- var state ServerState
+ var state serverState
if err := protocol.Call(ctx, serverConn, sessionsMethod, nil, &state); err != nil {
return nil, fmt.Errorf("querying server state: %w", err)
}
@@ -159,7 +144,7 @@ func QueryServerState(ctx context.Context, addr string) (*ServerState, error) {
// or auto://...).
func dialRemote(ctx context.Context, addr string) (jsonrpc2.Conn, error) {
network, address := ParseAddr(addr)
- if network == AutoNetwork {
+ if network == autoNetwork {
gp, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("getting gopls path: %w", err)
@@ -175,7 +160,10 @@ func dialRemote(ctx context.Context, addr string) (jsonrpc2.Conn, error) {
return serverConn, nil
}
-func ExecuteCommand(ctx context.Context, addr string, id string, request, result interface{}) error {
+// ExecuteCommand connects to the named server, sends it a
+// workspace/executeCommand request (with command 'id' and arguments
+// JSON encoded in 'request'), and populates the result variable.
+func ExecuteCommand(ctx context.Context, addr string, id string, request, result any) error {
serverConn, err := dialRemote(ctx, addr)
if err != nil {
return err
@@ -193,7 +181,7 @@ func ExecuteCommand(ctx context.Context, addr string, id string, request, result
// ServeStream dials the forwarder remote and binds the remote to serve the LSP
// on the incoming stream.
-func (f *Forwarder) ServeStream(ctx context.Context, clientConn jsonrpc2.Conn) error {
+func (f *forwarder) ServeStream(ctx context.Context, clientConn jsonrpc2.Conn) error {
client := protocol.ClientDispatcher(clientConn)
netConn, err := f.dialer.dialNet(ctx)
@@ -243,7 +231,7 @@ func (f *Forwarder) ServeStream(ctx context.Context, clientConn jsonrpc2.Conn) e
}
// TODO(rfindley): remove this handshaking in favor of middleware.
-func (f *Forwarder) handshake(ctx context.Context) {
+func (f *forwarder) handshake(ctx context.Context) {
// This call to os.Executable is redundant, and will be eliminated by the
// transition to the V2 API.
goplsPath, err := os.Executable()
@@ -280,7 +268,7 @@ func (f *Forwarder) handshake(ctx context.Context) {
}
func ConnectToRemote(ctx context.Context, addr string) (net.Conn, error) {
- dialer, err := NewAutoDialer(addr, nil)
+ dialer, err := newAutoDialer(addr, nil)
if err != nil {
return nil, err
}
@@ -289,7 +277,7 @@ func ConnectToRemote(ctx context.Context, addr string) (net.Conn, error) {
// handler intercepts messages to the daemon to enrich them with local
// information.
-func (f *Forwarder) handler(handler jsonrpc2.Handler) jsonrpc2.Handler {
+func (f *forwarder) handler(handler jsonrpc2.Handler) jsonrpc2.Handler {
return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error {
// Intercept certain messages to add special handling.
switch r.Method() {
@@ -374,7 +362,7 @@ func addGoEnvToInitializeRequest(ctx context.Context, r jsonrpc2.Request) (jsonr
return jsonrpc2.NewCall(call.ID(), "initialize", params)
}
-func (f *Forwarder) replyWithDebugAddress(outerCtx context.Context, r jsonrpc2.Replier, args command.DebuggingArgs) jsonrpc2.Replier {
+func (f *forwarder) replyWithDebugAddress(outerCtx context.Context, r jsonrpc2.Replier, args command.DebuggingArgs) jsonrpc2.Replier {
di := debug.GetInstance(outerCtx)
if di == nil {
event.Log(outerCtx, "no debug instance to start")
@@ -440,24 +428,24 @@ type handshakeResponse struct {
GoplsPath string `json:"goplsPath"`
}
-// ClientSession identifies a current client LSP session on the server. Note
+// clientSession identifies a current client LSP session on the server. Note
// that it looks similar to handshakeResposne, but in fact 'Logfile' and
// 'DebugAddr' now refer to the client.
-type ClientSession struct {
+type clientSession struct {
SessionID string `json:"sessionID"`
Logfile string `json:"logfile"`
DebugAddr string `json:"debugAddr"`
}
-// ServerState holds information about the gopls daemon process, including its
+// serverState holds information about the gopls daemon process, including its
// debug information and debug information of all of its current connected
// clients.
-type ServerState struct {
+type serverState struct {
Logfile string `json:"logfile"`
DebugAddr string `json:"debugAddr"`
GoplsPath string `json:"goplsPath"`
CurrentClientID string `json:"currentClientID"`
- Clients []ClientSession `json:"clients"`
+ Clients []clientSession `json:"clients"`
}
const (
@@ -501,7 +489,7 @@ func handshaker(session *cache.Session, goplsPath string, logHandshakes bool, ha
return reply(ctx, resp, nil)
case sessionsMethod:
- resp := ServerState{
+ resp := serverState{
GoplsPath: goplsPath,
CurrentClientID: session.ID(),
}
@@ -509,7 +497,7 @@ func handshaker(session *cache.Session, goplsPath string, logHandshakes bool, ha
resp.Logfile = di.Logfile
resp.DebugAddr = di.ListenedDebugAddress()
for _, c := range di.State.Clients() {
- resp.Clients = append(resp.Clients, ClientSession{
+ resp.Clients = append(resp.Clients, clientSession{
SessionID: c.Session.ID(),
Logfile: c.Logfile,
DebugAddr: c.DebugAddress,
@@ -535,8 +523,8 @@ func sendError(ctx context.Context, reply jsonrpc2.Replier, err error) {
func ParseAddr(listen string) (network string, address string) {
// Allow passing just -remote=auto, as a shorthand for using automatic remote
// resolution.
- if listen == AutoNetwork {
- return AutoNetwork, ""
+ if listen == autoNetwork {
+ return autoNetwork, ""
}
if parts := strings.SplitN(listen, ";", 2); len(parts) == 2 {
return parts[0], parts[1]
diff --git a/gopls/internal/lsp/lsprpc/lsprpc_test.go b/gopls/internal/lsprpc/lsprpc_test.go
similarity index 96%
rename from gopls/internal/lsp/lsprpc/lsprpc_test.go
rename to gopls/internal/lsprpc/lsprpc_test.go
index 04d1be5c1f4..1d643bf2095 100644
--- a/gopls/internal/lsp/lsprpc/lsprpc_test.go
+++ b/gopls/internal/lsprpc/lsprpc_test.go
@@ -13,9 +13,9 @@ import (
"testing"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/jsonrpc2"
@@ -58,8 +58,8 @@ func TestClientLogging(t *testing.T) {
server := PingServer{}
client := FakeClient{Logs: make(chan string, 10)}
- ctx = debug.WithInstance(ctx, "", "")
- ss := NewStreamServer(cache.New(nil), false, nil)
+ ctx = debug.WithInstance(ctx, "")
+ ss := NewStreamServer(cache.New(nil), false, nil).(*StreamServer)
ss.serverForTest = server
ts := servertest.NewPipeServer(ss, nil)
defer checkClose(t, ts.Close)
@@ -121,8 +121,8 @@ 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, nil)
+ serveCtx := debug.WithInstance(ctx, "")
+ ss := NewStreamServer(cache.New(nil), false, nil).(*StreamServer)
ss.serverForTest = s
tsDirect := servertest.NewTCPServer(serveCtx, ss, nil)
@@ -214,8 +214,8 @@ func TestDebugInfoLifecycle(t *testing.T) {
baseCtx, cancel := context.WithCancel(context.Background())
defer cancel()
- clientCtx := debug.WithInstance(baseCtx, "", "")
- serverCtx := debug.WithInstance(baseCtx, "", "")
+ clientCtx := debug.WithInstance(baseCtx, "")
+ serverCtx := debug.WithInstance(baseCtx, "")
cache := cache.New(nil)
ss := NewStreamServer(cache, false, nil)
diff --git a/gopls/internal/lsp/lsprpc/middleware.go b/gopls/internal/lsprpc/middleware_test.go
similarity index 55%
rename from gopls/internal/lsp/lsprpc/middleware.go
rename to gopls/internal/lsprpc/middleware_test.go
index 50089cde7dc..526c7343b78 100644
--- a/gopls/internal/lsp/lsprpc/middleware.go
+++ b/gopls/internal/lsprpc/middleware_test.go
@@ -2,20 +2,114 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package lsprpc
+package lsprpc_test
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"sync"
+ "testing"
+ "time"
+ . "golang.org/x/tools/gopls/internal/lsprpc"
"golang.org/x/tools/internal/event"
jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2"
)
-// Metadata holds arbitrary data transferred between jsonrpc2 peers.
-type Metadata map[string]interface{}
+var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions {
+ return jsonrpc2_v2.ConnectionOptions{}
+})
+
+func TestHandshakeMiddleware(t *testing.T) {
+ sh := &Handshaker{
+ metadata: metadata{
+ "answer": 42,
+ },
+ }
+ ctx := context.Background()
+ env := new(TestEnv)
+ defer env.Shutdown(t)
+ l, _ := env.serve(ctx, t, sh.Middleware(noopBinder))
+ conn := env.dial(ctx, t, l.Dialer(), noopBinder, false)
+ ch := &Handshaker{
+ metadata: metadata{
+ "question": 6 * 9,
+ },
+ }
+
+ check := func(connected bool) error {
+ clients := sh.Peers()
+ servers := ch.Peers()
+ want := 0
+ if connected {
+ want = 1
+ }
+ if got := len(clients); got != want {
+ return fmt.Errorf("got %d clients on the server, want %d", got, want)
+ }
+ if got := len(servers); got != want {
+ return fmt.Errorf("got %d servers on the client, want %d", got, want)
+ }
+ if !connected {
+ return nil
+ }
+ client := clients[0]
+ server := servers[0]
+ if _, ok := client.Metadata["question"]; !ok {
+ return errors.New("no client metadata")
+ }
+ if _, ok := server.Metadata["answer"]; !ok {
+ return errors.New("no server metadata")
+ }
+ if client.LocalID != server.RemoteID {
+ return fmt.Errorf("client.LocalID == %d, server.PeerID == %d", client.LocalID, server.RemoteID)
+ }
+ if client.RemoteID != server.LocalID {
+ return fmt.Errorf("client.PeerID == %d, server.LocalID == %d", client.RemoteID, server.LocalID)
+ }
+ return nil
+ }
+
+ if err := check(false); err != nil {
+ t.Fatalf("before handshake: %v", err)
+ }
+ ch.ClientHandshake(ctx, conn)
+ if err := check(true); err != nil {
+ t.Fatalf("after handshake: %v", err)
+ }
+ conn.Close()
+ // Wait for up to ~2s for connections to get cleaned up.
+ delay := 25 * time.Millisecond
+ for retries := 3; retries >= 0; retries-- {
+ time.Sleep(delay)
+ err := check(false)
+ if err == nil {
+ return
+ }
+ if retries == 0 {
+ t.Fatalf("after closing connection: %v", err)
+ }
+ delay *= 4
+ }
+}
+
+// Handshaker handles both server and client handshaking over jsonrpc2 v2.
+// To instrument server-side handshaking, use Handshaker.Middleware.
+// To instrument client-side handshaking, call
+// Handshaker.ClientHandshake for any new client-side connections.
+type Handshaker struct {
+ // metadata will be shared with peers via handshaking.
+ metadata metadata
+
+ mu sync.Mutex
+ prevID int64
+ peers map[int64]PeerInfo
+}
+
+// metadata holds arbitrary data transferred between jsonrpc2 peers.
+type metadata map[string]any
// PeerInfo holds information about a peering between jsonrpc2 servers.
type PeerInfo struct {
@@ -30,20 +124,7 @@ type PeerInfo struct {
IsClient bool
// Metadata holds arbitrary information provided by the peer.
- Metadata Metadata
-}
-
-// Handshaker handles both server and client handshaking over jsonrpc2. To
-// instrument server-side handshaking, use Handshaker.Middleware. To instrument
-// client-side handshaking, call Handshaker.ClientHandshake for any new
-// client-side connections.
-type Handshaker struct {
- // Metadata will be shared with peers via handshaking.
- Metadata Metadata
-
- mu sync.Mutex
- prevID int64
- peers map[int64]PeerInfo
+ Metadata metadata
}
// Peers returns the peer info this handshaker knows about by way of either the
@@ -68,13 +149,13 @@ func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder {
localID := h.nextID()
info := &PeerInfo{
RemoteID: localID,
- Metadata: h.Metadata,
+ Metadata: h.metadata,
}
// Wrap the delegated handler to accept the handshake.
delegate := opts.Handler
opts.Handler = jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) {
- if req.Method == handshakeMethod {
+ if req.Method == HandshakeMethod {
var peerInfo PeerInfo
if err := json.Unmarshal(req.Params, &peerInfo); err != nil {
return nil, fmt.Errorf("%w: unmarshaling client info: %v", jsonrpc2_v2.ErrInvalidParams, err)
@@ -101,10 +182,10 @@ func (h *Handshaker) ClientHandshake(ctx context.Context, conn *jsonrpc2_v2.Conn
localID := h.nextID()
info := &PeerInfo{
RemoteID: localID,
- Metadata: h.Metadata,
+ Metadata: h.metadata,
}
- call := conn.Call(ctx, handshakeMethod, info)
+ call := conn.Call(ctx, HandshakeMethod, info)
var serverInfo PeerInfo
if err := call.Await(ctx, &serverInfo); err != nil {
event.Error(ctx, "performing handshake", err)
diff --git a/gopls/internal/mod/code_lens.go b/gopls/internal/mod/code_lens.go
index 6feb93e4f18..85d8182e8fe 100644
--- a/gopls/internal/mod/code_lens.go
+++ b/gopls/internal/mod/code_lens.go
@@ -11,16 +11,16 @@ import (
"path/filepath"
"golang.org/x/mod/modfile"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "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/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
)
// LensFuncs returns the supported lensFuncs for go.mod files.
-func LensFuncs() map[command.Command]source.LensFunc {
- return map[command.Command]source.LensFunc{
+func LensFuncs() map[command.Command]golang.LensFunc {
+ return map[command.Command]golang.LensFunc{
command.UpgradeDependency: upgradeLenses,
command.Tidy: tidyLens,
command.Vendor: vendorLens,
diff --git a/gopls/internal/mod/diagnostics.go b/gopls/internal/mod/diagnostics.go
index c80a56d8115..655beedeb27 100644
--- a/gopls/internal/mod/diagnostics.go
+++ b/gopls/internal/mod/diagnostics.go
@@ -17,10 +17,10 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
"golang.org/x/sync/errgroup"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "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/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/vulncheck/govulncheck"
"golang.org/x/tools/internal/event"
diff --git a/gopls/internal/mod/format.go b/gopls/internal/mod/format.go
index 8bb40852287..14408393969 100644
--- a/gopls/internal/mod/format.go
+++ b/gopls/internal/mod/format.go
@@ -7,9 +7,9 @@ package mod
import (
"context"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/mod/hover.go b/gopls/internal/mod/hover.go
index 44f32dcea4a..458c5ce67d5 100644
--- a/gopls/internal/mod/hover.go
+++ b/gopls/internal/mod/hover.go
@@ -13,9 +13,9 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/gopls/internal/vulncheck/govulncheck"
diff --git a/gopls/internal/mod/inlayhint.go b/gopls/internal/mod/inlayhint.go
index 6c0275e9405..73286be4be6 100644
--- a/gopls/internal/mod/inlayhint.go
+++ b/gopls/internal/mod/inlayhint.go
@@ -8,9 +8,9 @@ import (
"fmt"
"golang.org/x/mod/modfile"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, _ protocol.Range) ([]protocol.InlayHint, error) {
diff --git a/gopls/internal/lsp/progress/progress.go b/gopls/internal/progress/progress.go
similarity index 83%
rename from gopls/internal/lsp/progress/progress.go
rename to gopls/internal/progress/progress.go
index 6ccf086df13..0bb17b35669 100644
--- a/gopls/internal/lsp/progress/progress.go
+++ b/gopls/internal/progress/progress.go
@@ -2,22 +2,36 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// The progress package defines utilities for reporting the progress
+// of long-running operations using features of the LSP client
+// interface such as Progress and ShowMessage.
package progress
import (
"context"
"fmt"
+ "io"
"math/rand"
"strconv"
"strings"
"sync"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/internal/xcontext"
)
+// NewTracker returns a new Tracker that reports progress to the
+// specified client.
+func NewTracker(client protocol.Client) *Tracker {
+ return &Tracker{
+ client: client,
+ inProgress: make(map[protocol.ProgressToken]*WorkDone),
+ }
+}
+
+// A Tracker reports the progress of a long-running operation to an LSP client.
type Tracker struct {
client protocol.Client
supportsWorkDoneProgress bool
@@ -26,27 +40,20 @@ type Tracker struct {
inProgress map[protocol.ProgressToken]*WorkDone
}
-func NewTracker(client protocol.Client) *Tracker {
- return &Tracker{
- client: client,
- inProgress: make(map[protocol.ProgressToken]*WorkDone),
- }
-}
-
-// SetSupportsWorkDoneProgress sets whether the client supports work done
+// SetSupportsWorkDoneProgress sets whether the client supports "work done"
// progress reporting. It must be set before using the tracker.
//
// TODO(rfindley): fix this broken initialization pattern.
// Also: do we actually need the fall-back progress behavior using ShowMessage?
// Surely ShowMessage notifications are too noisy to be worthwhile.
-func (tracker *Tracker) SetSupportsWorkDoneProgress(b bool) {
- tracker.supportsWorkDoneProgress = b
+func (t *Tracker) SetSupportsWorkDoneProgress(b bool) {
+ t.supportsWorkDoneProgress = b
}
// SupportsWorkDoneProgress reports whether the tracker supports work done
// progress reporting.
-func (tracker *Tracker) SupportsWorkDoneProgress() bool {
- return tracker.supportsWorkDoneProgress
+func (t *Tracker) SupportsWorkDoneProgress() bool {
+ return t.supportsWorkDoneProgress
}
// Start notifies the client of work being done on the server. It uses either
@@ -247,36 +254,38 @@ func (wd *WorkDone) End(ctx context.Context, message string) {
}
}
-// EventWriter writes every incoming []byte to
-// event.Print with the operation=generate tag
-// to distinguish its logs from others.
-type EventWriter struct {
- ctx context.Context
- operation string
+// NewEventWriter returns an [io.Writer] that calls the context's
+// event printer for each data payload, wrapping it with the
+// operation=generate tag to distinguish its logs from others.
+func NewEventWriter(ctx context.Context, operation string) io.Writer {
+ return &eventWriter{ctx: ctx, operation: operation}
}
-func NewEventWriter(ctx context.Context, operation string) *EventWriter {
- return &EventWriter{ctx: ctx, operation: operation}
+type eventWriter struct {
+ ctx context.Context
+ operation string
}
-func (ew *EventWriter) Write(p []byte) (n int, err error) {
+func (ew *eventWriter) Write(p []byte) (n int, err error) {
event.Log(ew.ctx, string(p), tag.Operation.Of(ew.operation))
return len(p), nil
}
-// WorkDoneWriter wraps a workDone handle to provide a Writer interface,
+// NewWorkDoneWriter wraps a WorkDone handle to provide a Writer interface,
// so that workDone reporting can more easily be hooked into commands.
-type WorkDoneWriter struct {
+func NewWorkDoneWriter(ctx context.Context, wd *WorkDone) io.Writer {
+ return &workDoneWriter{ctx: ctx, wd: wd}
+}
+
+// workDoneWriter wraps a workDone handle to provide a Writer interface,
+// so that workDone reporting can more easily be hooked into commands.
+type workDoneWriter struct {
// In order to implement the io.Writer interface, we must close over ctx.
ctx context.Context
wd *WorkDone
}
-func NewWorkDoneWriter(ctx context.Context, wd *WorkDone) *WorkDoneWriter {
- return &WorkDoneWriter{ctx: ctx, wd: wd}
-}
-
-func (wdw *WorkDoneWriter) Write(p []byte) (n int, err error) {
+func (wdw *workDoneWriter) Write(p []byte) (n int, err error) {
wdw.wd.Report(wdw.ctx, string(p), 0)
// Don't fail just because of a failure to report progress.
return len(p), nil
diff --git a/gopls/internal/lsp/progress/progress_test.go b/gopls/internal/progress/progress_test.go
similarity index 95%
rename from gopls/internal/lsp/progress/progress_test.go
rename to gopls/internal/progress/progress_test.go
index ef87eba121a..642103ae025 100644
--- a/gopls/internal/lsp/progress/progress_test.go
+++ b/gopls/internal/progress/progress_test.go
@@ -10,7 +10,7 @@ import (
"sync"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
type fakeClient struct {
@@ -63,7 +63,7 @@ func (c *fakeClient) ShowMessage(context.Context, *protocol.ShowMessageParams) e
return nil
}
-func setup(token protocol.ProgressToken) (context.Context, *Tracker, *fakeClient) {
+func setup() (context.Context, *Tracker, *fakeClient) {
c := &fakeClient{}
tracker := NewTracker(c)
tracker.SetSupportsWorkDoneProgress(true)
@@ -109,7 +109,7 @@ func TestProgressTracker_Reporting(t *testing.T) {
} {
test := test
t.Run(test.name, func(t *testing.T) {
- ctx, tracker, client := setup(test.token)
+ ctx, tracker, client := setup()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
tracker.supportsWorkDoneProgress = test.supported
@@ -147,7 +147,7 @@ func TestProgressTracker_Reporting(t *testing.T) {
func TestProgressTracker_Cancellation(t *testing.T) {
for _, token := range []protocol.ProgressToken{nil, 1, "a"} {
- ctx, tracker, _ := setup(token)
+ ctx, tracker, _ := setup()
var canceled bool
cancel := func() { canceled = true }
work := tracker.Start(ctx, "work", "message", token, cancel)
diff --git a/gopls/internal/lsp/protocol/codeactionkind.go b/gopls/internal/protocol/codeactionkind.go
similarity index 100%
rename from gopls/internal/lsp/protocol/codeactionkind.go
rename to gopls/internal/protocol/codeactionkind.go
diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go
similarity index 99%
rename from gopls/internal/lsp/command/command_gen.go
rename to gopls/internal/protocol/command/command_gen.go
index fb518c71860..2ee0a5d19be 100644
--- a/gopls/internal/lsp/command/command_gen.go
+++ b/gopls/internal/protocol/command/command_gen.go
@@ -15,7 +15,7 @@ import (
"context"
"fmt"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// Symbolic names for gopls commands, excluding "gopls." prefix.
@@ -118,13 +118,13 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
return nil, err
}
- return nil, s.ApplyFix(ctx, a0)
+ return s.ApplyFix(ctx, a0)
case "gopls.change_signature":
var a0 ChangeSignatureArgs
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
return nil, err
}
- return nil, s.ChangeSignature(ctx, a0)
+ return s.ChangeSignature(ctx, a0)
case "gopls.check_upgrades":
var a0 CheckUpgradesArgs
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
diff --git a/gopls/internal/lsp/command/commandmeta/meta.go b/gopls/internal/protocol/command/commandmeta/meta.go
similarity index 98%
rename from gopls/internal/lsp/command/commandmeta/meta.go
rename to gopls/internal/protocol/command/commandmeta/meta.go
index bf85c4faa9b..d468534ffe0 100644
--- a/gopls/internal/lsp/command/commandmeta/meta.go
+++ b/gopls/internal/protocol/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/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/protocol/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/gopls/internal/lsp/command",
+ "golang.org/x/tools/gopls/internal/protocol/command",
)
if err != nil {
return nil, nil, fmt.Errorf("packages.Load: %v", err)
diff --git a/gopls/internal/lsp/command/gen/gen.go b/gopls/internal/protocol/command/gen/gen.go
similarity index 94%
rename from gopls/internal/lsp/command/gen/gen.go
rename to gopls/internal/protocol/command/gen/gen.go
index 3dde3546f41..15ff467f3e3 100644
--- a/gopls/internal/lsp/command/gen/gen.go
+++ b/gopls/internal/protocol/command/gen/gen.go
@@ -12,7 +12,7 @@ import (
"go/types"
"text/template"
- "golang.org/x/tools/gopls/internal/lsp/command/commandmeta"
+ "golang.org/x/tools/gopls/internal/protocol/command/commandmeta"
"golang.org/x/tools/internal/imports"
)
@@ -112,10 +112,10 @@ func Generate() ([]byte, error) {
Imports: map[string]bool{
"context": true,
"fmt": true,
- "golang.org/x/tools/gopls/internal/lsp/protocol": true,
+ "golang.org/x/tools/gopls/internal/protocol": true,
},
}
- const thispkg = "golang.org/x/tools/gopls/internal/lsp/command"
+ const thispkg = "golang.org/x/tools/gopls/internal/protocol/command"
for _, c := range d.Commands {
for _, arg := range c.Args {
pth := pkgPath(arg.Type)
diff --git a/gopls/internal/lsp/command/generate.go b/gopls/internal/protocol/command/generate.go
similarity index 87%
rename from gopls/internal/lsp/command/generate.go
rename to gopls/internal/protocol/command/generate.go
index b7907e60f5c..f63b2e6e5ba 100644
--- a/gopls/internal/lsp/command/generate.go
+++ b/gopls/internal/protocol/command/generate.go
@@ -11,7 +11,7 @@ import (
"log"
"os"
- "golang.org/x/tools/gopls/internal/lsp/command/gen"
+ "golang.org/x/tools/gopls/internal/protocol/command/gen"
)
func main() {
diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/protocol/command/interface.go
similarity index 95%
rename from gopls/internal/lsp/command/interface.go
rename to gopls/internal/protocol/command/interface.go
index 152387c2053..b0dd06088bd 100644
--- a/gopls/internal/lsp/command/interface.go
+++ b/gopls/internal/protocol/command/interface.go
@@ -17,7 +17,7 @@ package command
import (
"context"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/vulncheck"
)
@@ -44,7 +44,7 @@ type Interface interface {
// ApplyFix: Apply a fix
//
// Applies a fix to a region of source code.
- ApplyFix(context.Context, ApplyFixArgs) error
+ ApplyFix(context.Context, ApplyFixArgs) (*protocol.WorkspaceEdit, error)
// Test: Run test(s) (legacy)
//
@@ -216,7 +216,7 @@ type Interface interface {
//
// This command is experimental, currently only supporting parameter removal.
// Its signature will certainly change in the future (pun intended).
- ChangeSignature(context.Context, ChangeSignatureArgs) error
+ ChangeSignature(context.Context, ChangeSignatureArgs) (*protocol.WorkspaceEdit, error)
// DiagnoseFiles: Cause server to publish diagnostics for the specified files.
//
@@ -251,12 +251,22 @@ type GenerateArgs struct {
// TODO(rFindley): document the rest of these once the docgen is fleshed out.
type ApplyFixArgs struct {
- // The fix to apply.
+ // The name of the fix to apply.
+ //
+ // For fixes suggested by analyzers, this is a string constant
+ // advertised by the analyzer that matches the Category of
+ // the analysis.Diagnostic with a SuggestedFix containing no edits.
+ //
+ // For fixes suggested by code actions, this is a string agreed
+ // upon by the code action and golang.ApplyFix.
Fix string
+
// The file URI for the document to fix.
URI protocol.DocumentURI
// The document range to scan for fixes.
Range protocol.Range
+ // Whether to resolve and return the edits.
+ ResolveEdits bool
}
type URIArg struct {
@@ -500,6 +510,8 @@ type AddTelemetryCountersArgs struct {
// ChangeSignatureArgs specifies a "change signature" refactoring to perform.
type ChangeSignatureArgs struct {
RemoveParameter protocol.Location
+ // Whether to resolve and return the edits.
+ ResolveEdits bool
}
// DiagnoseFilesArgs specifies a set of files for which diagnostics are wanted.
diff --git a/gopls/internal/lsp/command/interface_test.go b/gopls/internal/protocol/command/interface_test.go
similarity index 92%
rename from gopls/internal/lsp/command/interface_test.go
rename to gopls/internal/protocol/command/interface_test.go
index f81a2aa22fd..4ddc5fa2e67 100644
--- a/gopls/internal/lsp/command/interface_test.go
+++ b/gopls/internal/protocol/command/interface_test.go
@@ -9,7 +9,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/command/gen"
+ "golang.org/x/tools/gopls/internal/protocol/command/gen"
"golang.org/x/tools/internal/testenv"
)
diff --git a/gopls/internal/lsp/command/util.go b/gopls/internal/protocol/command/util.go
similarity index 100%
rename from gopls/internal/lsp/command/util.go
rename to gopls/internal/protocol/command/util.go
diff --git a/gopls/internal/lsp/protocol/context.go b/gopls/internal/protocol/context.go
similarity index 64%
rename from gopls/internal/lsp/protocol/context.go
rename to gopls/internal/protocol/context.go
index a9ef48d0f0b..5f3151cda97 100644
--- a/gopls/internal/lsp/protocol/context.go
+++ b/gopls/internal/protocol/context.go
@@ -7,6 +7,7 @@ package protocol
import (
"bytes"
"context"
+ "sync"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/core"
@@ -38,8 +39,27 @@ func LogEvent(ctx context.Context, ev core.Event, lm label.Map, mt MessageType)
if event.IsError(ev) {
msg.Type = Error
}
- // TODO(adonovan): the goroutine here could cause log
- // messages to be delivered out of order! Use a queue.
- go client.LogMessage(xcontext.Detach(ctx), msg)
+
+ // The background goroutine lives forever once started,
+ // and ensures log messages are sent in order (#61216).
+ startLogSenderOnce.Do(func() {
+ go func() {
+ for f := range logQueue {
+ f()
+ }
+ }()
+ })
+
+ // Add the log item to a queue, rather than sending a
+ // window/logMessage request to the client synchronously,
+ // which would slow down this thread.
+ ctx2 := xcontext.Detach(ctx)
+ logQueue <- func() { client.LogMessage(ctx2, msg) }
+
return ctx
}
+
+var (
+ startLogSenderOnce sync.Once
+ logQueue = make(chan func(), 100) // big enough for a large transient burst
+)
diff --git a/gopls/internal/lsp/protocol/doc.go b/gopls/internal/protocol/doc.go
similarity index 100%
rename from gopls/internal/lsp/protocol/doc.go
rename to gopls/internal/protocol/doc.go
diff --git a/gopls/internal/lsp/protocol/edits.go b/gopls/internal/protocol/edits.go
similarity index 76%
rename from gopls/internal/lsp/protocol/edits.go
rename to gopls/internal/protocol/edits.go
index 71d4427c73c..53fd4cf94e3 100644
--- a/gopls/internal/lsp/protocol/edits.go
+++ b/gopls/internal/protocol/edits.go
@@ -101,3 +101,28 @@ func AsAnnotatedTextEdits(edits []TextEdit) []Or_TextDocumentEdit_edits_Elem {
}
return result
}
+
+// TextEditsToDocumentChanges converts a set of edits within the
+// specified (versioned) file to a singleton list of DocumentChanges
+// (as required for a WorkspaceEdit).
+func TextEditsToDocumentChanges(uri DocumentURI, version int32, edits []TextEdit) []DocumentChanges {
+ return []DocumentChanges{{
+ TextDocumentEdit: &TextDocumentEdit{
+ TextDocument: OptionalVersionedTextDocumentIdentifier{
+ Version: version,
+ TextDocumentIdentifier: TextDocumentIdentifier{URI: uri},
+ },
+ Edits: AsAnnotatedTextEdits(edits),
+ },
+ }}
+}
+
+// TextDocumentEditsToDocumentChanges wraps each TextDocumentEdit in a DocumentChange.
+func TextDocumentEditsToDocumentChanges(edits []TextDocumentEdit) []DocumentChanges {
+ changes := []DocumentChanges{} // non-nil
+ for _, edit := range edits {
+ edit := edit
+ changes = append(changes, DocumentChanges{TextDocumentEdit: &edit})
+ }
+ return changes
+}
diff --git a/gopls/internal/lsp/protocol/enums.go b/gopls/internal/protocol/enums.go
similarity index 86%
rename from gopls/internal/lsp/protocol/enums.go
rename to gopls/internal/protocol/enums.go
index 87c14d8d553..e3f8b515542 100644
--- a/gopls/internal/lsp/protocol/enums.go
+++ b/gopls/internal/protocol/enums.go
@@ -117,7 +117,7 @@ func init() {
namesTextDocumentSaveReason[int(FocusOut)] = "FocusOut"
}
-func formatEnum(f fmt.State, c rune, i int, names []string, unknown string) {
+func formatEnum(f fmt.State, i int, names []string, unknown string) {
s := ""
if i >= 0 && i < len(names) {
s = names[i]
@@ -130,45 +130,45 @@ func formatEnum(f fmt.State, c rune, i int, names []string, unknown string) {
}
func (e TextDocumentSyncKind) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesTextDocumentSyncKind[:], "TextDocumentSyncKind")
+ formatEnum(f, int(e), namesTextDocumentSyncKind[:], "TextDocumentSyncKind")
}
func (e MessageType) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesMessageType[:], "MessageType")
+ formatEnum(f, int(e), namesMessageType[:], "MessageType")
}
func (e FileChangeType) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesFileChangeType[:], "FileChangeType")
+ formatEnum(f, int(e), namesFileChangeType[:], "FileChangeType")
}
func (e CompletionTriggerKind) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesCompletionTriggerKind[:], "CompletionTriggerKind")
+ formatEnum(f, int(e), namesCompletionTriggerKind[:], "CompletionTriggerKind")
}
func (e DiagnosticSeverity) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesDiagnosticSeverity[:], "DiagnosticSeverity")
+ formatEnum(f, int(e), namesDiagnosticSeverity[:], "DiagnosticSeverity")
}
func (e DiagnosticTag) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesDiagnosticTag[:], "DiagnosticTag")
+ formatEnum(f, int(e), namesDiagnosticTag[:], "DiagnosticTag")
}
func (e CompletionItemKind) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesCompletionItemKind[:], "CompletionItemKind")
+ formatEnum(f, int(e), namesCompletionItemKind[:], "CompletionItemKind")
}
func (e InsertTextFormat) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesInsertTextFormat[:], "InsertTextFormat")
+ formatEnum(f, int(e), namesInsertTextFormat[:], "InsertTextFormat")
}
func (e DocumentHighlightKind) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesDocumentHighlightKind[:], "DocumentHighlightKind")
+ formatEnum(f, int(e), namesDocumentHighlightKind[:], "DocumentHighlightKind")
}
func (e SymbolKind) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesSymbolKind[:], "SymbolKind")
+ formatEnum(f, int(e), namesSymbolKind[:], "SymbolKind")
}
func (e TextDocumentSaveReason) Format(f fmt.State, c rune) {
- formatEnum(f, c, int(e), namesTextDocumentSaveReason[:], "TextDocumentSaveReason")
+ formatEnum(f, int(e), namesTextDocumentSaveReason[:], "TextDocumentSaveReason")
}
diff --git a/gopls/internal/lsp/protocol/generate/README.md b/gopls/internal/protocol/generate/README.md
similarity index 100%
rename from gopls/internal/lsp/protocol/generate/README.md
rename to gopls/internal/protocol/generate/README.md
diff --git a/gopls/internal/lsp/protocol/generate/generate.go b/gopls/internal/protocol/generate/generate.go
similarity index 100%
rename from gopls/internal/lsp/protocol/generate/generate.go
rename to gopls/internal/protocol/generate/generate.go
diff --git a/gopls/internal/lsp/protocol/generate/main.go b/gopls/internal/protocol/generate/main.go
similarity index 96%
rename from gopls/internal/lsp/protocol/generate/main.go
rename to gopls/internal/protocol/generate/main.go
index cf89a528edf..f70c5810d6c 100644
--- a/gopls/internal/lsp/protocol/generate/main.go
+++ b/gopls/internal/protocol/generate/main.go
@@ -100,7 +100,6 @@ func writeclient() {
`import (
"context"
- "golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/jsonrpc2"
)
`)
@@ -110,12 +109,7 @@ func writeclient() {
}
out.WriteString("}\n\n")
out.WriteString(`func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) {
- defer func() {
- if x := recover(); x != nil {
- bug.Reportf("client panic in %s request", r.Method())
- panic(x)
- }
- }()
+ defer recoverHandlerPanic(r.Method())
switch r.Method() {
`)
for _, k := range ccases.keys() {
@@ -144,7 +138,6 @@ func writeserver() {
`import (
"context"
- "golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/jsonrpc2"
)
`)
@@ -156,12 +149,7 @@ func writeserver() {
}
func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) {
- defer func() {
- if x := recover(); x != nil {
- bug.Reportf("server panic in %s request", r.Method())
- panic(x)
- }
- }()
+ defer recoverHandlerPanic(r.Method())
switch r.Method() {
`)
for _, k := range scases.keys() {
diff --git a/gopls/internal/lsp/protocol/generate/main_test.go b/gopls/internal/protocol/generate/main_test.go
similarity index 100%
rename from gopls/internal/lsp/protocol/generate/main_test.go
rename to gopls/internal/protocol/generate/main_test.go
diff --git a/gopls/internal/lsp/protocol/generate/output.go b/gopls/internal/protocol/generate/output.go
similarity index 99%
rename from gopls/internal/lsp/protocol/generate/output.go
rename to gopls/internal/protocol/generate/output.go
index 18fc85c01d9..fc64677ff8e 100644
--- a/gopls/internal/lsp/protocol/generate/output.go
+++ b/gopls/internal/protocol/generate/output.go
@@ -101,7 +101,7 @@ func genCase(method string, param, result *Type, dir string) {
nm = "ParamConfiguration" // gopls compatibility
}
fmt.Fprintf(out, "\t\tvar params %s\n", nm)
- fmt.Fprintf(out, "\t\tif err := unmarshalParams(r.Params(), ¶ms); err != nil {\n")
+ fmt.Fprintf(out, "\t\tif err := UnmarshalJSON(r.Params(), ¶ms); err != nil {\n")
fmt.Fprintf(out, "\t\t\treturn true, sendParseError(ctx, reply, err)\n\t\t}\n")
p = ", ¶ms"
}
diff --git a/gopls/internal/lsp/protocol/generate/tables.go b/gopls/internal/protocol/generate/tables.go
similarity index 97%
rename from gopls/internal/lsp/protocol/generate/tables.go
rename to gopls/internal/protocol/generate/tables.go
index 2b744a5a052..ac428b58479 100644
--- a/gopls/internal/lsp/protocol/generate/tables.go
+++ b/gopls/internal/protocol/generate/tables.go
@@ -63,6 +63,7 @@ var renameProp = map[prop]string{
{"CancelParams", "id"}: "interface{}",
{"Command", "arguments"}: "[]json.RawMessage",
{"CompletionItem", "textEdit"}: "TextEdit",
+ {"CodeAction", "data"}: "json.RawMessage", // delay unmarshalling commands
{"Diagnostic", "code"}: "interface{}",
{"Diagnostic", "data"}: "json.RawMessage", // delay unmarshalling quickfixes
@@ -132,10 +133,9 @@ var goplsType = map[string]string{
"Or_Declaration": "[]Location",
"Or_DidChangeConfigurationRegistrationOptions_section": "OrPSection_workspace_didChangeConfiguration",
- "Or_GlobPattern": "string",
- "Or_InlayHintLabelPart_tooltip": "OrPTooltipPLabel",
- "Or_InlayHint_tooltip": "OrPTooltip_textDocument_inlayHint",
- "Or_LSPAny": "interface{}",
+ "Or_InlayHintLabelPart_tooltip": "OrPTooltipPLabel",
+ "Or_InlayHint_tooltip": "OrPTooltip_textDocument_inlayHint",
+ "Or_LSPAny": "interface{}",
"Or_ParameterInformation_documentation": "string",
"Or_ParameterInformation_label": "string",
@@ -150,6 +150,7 @@ var goplsType = map[string]string{
"Or_Result_textDocument_typeDefinition": "[]Location",
"Or_Result_workspace_symbol": "[]SymbolInformation",
"Or_TextDocumentContentChangeEvent": "TextDocumentContentChangePartial",
+ "Or_RelativePattern_baseUri": "DocumentURI",
"Or_WorkspaceFoldersServerCapabilities_changeNotifications": "string",
"Or_WorkspaceSymbol_location": "OrPLocation_workspace_symbol",
diff --git a/gopls/internal/lsp/protocol/generate/typenames.go b/gopls/internal/protocol/generate/typenames.go
similarity index 100%
rename from gopls/internal/lsp/protocol/generate/typenames.go
rename to gopls/internal/protocol/generate/typenames.go
diff --git a/gopls/internal/lsp/protocol/generate/types.go b/gopls/internal/protocol/generate/types.go
similarity index 100%
rename from gopls/internal/lsp/protocol/generate/types.go
rename to gopls/internal/protocol/generate/types.go
diff --git a/gopls/internal/lsp/protocol/json_test.go b/gopls/internal/protocol/json_test.go
similarity index 92%
rename from gopls/internal/lsp/protocol/json_test.go
rename to gopls/internal/protocol/json_test.go
index 2753125d454..9aac110fa3b 100644
--- a/gopls/internal/lsp/protocol/json_test.go
+++ b/gopls/internal/protocol/json_test.go
@@ -12,7 +12,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// verify that type errors in Initialize lsp messages don't cause
@@ -30,7 +30,7 @@ import (
// a recent Initialize message taken from a log (at some point
// some field incompatibly changed from bool to int32)
-const input = `{"processId":46408,"clientInfo":{"name":"Visual Studio Code - Insiders","version":"1.76.0-insider"},"locale":"en-us","rootPath":"/Users/pjw/hakim","rootUri":"file:///Users/pjw/hakim","capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional","normalizesLineEndings":true,"changeAnnotationSupport":{"groupsOnLabel":true}},"configuration":true,"didChangeWatchedFiles":{"dynamicRegistration":true,"relativePatternSupport":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]},"resolveSupport":{"properties":["location.range"]}},"codeLens":{"refreshSupport":true},"executeCommand":{"dynamicRegistration":true},"didChangeConfiguration":{"dynamicRegistration":true},"workspaceFolders":true,"semanticTokens":{"refreshSupport":true},"fileOperations":{"dynamicRegistration":true,"didCreate":true,"didRename":true,"didDelete":true,"willCreate":true,"willRename":true,"willDelete":true},"inlineValue":{"refreshSupport":true},"inlayHint":{"refreshSupport":true},"diagnostics":{"refreshSupport":true}},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"codeDescriptionSupport":true,"dataSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true,"resolveSupport":{"properties":["documentation","detail","additionalTextEdits"]},"insertTextModeSupport":{"valueSet":[1,2]},"labelDetailsSupport":true},"insertTextMode":2,"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]},"completionList":{"itemDefaults":["commitCharacters","editRange","insertTextFormat","insertTextMode"]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true},"activeParameterSupport":true},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]},"labelSupport":true},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"disabledSupport":true,"dataSupport":true,"resolveSupport":{"properties":["edit"]},"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"honorsChangeAnnotations":false},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true,"prepareSupportDefaultBehavior":1,"honorsChangeAnnotations":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true,"foldingRangeKind":{"valueSet":["comment","imports","region"]},"foldingRange":{"collapsedText":false}},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"callHierarchy":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator","decorator"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"formats":["relative"],"requests":{"range":true,"full":{"delta":true}},"multilineTokenSupport":false,"overlappingTokenSupport":false,"serverCancelSupport":true,"augmentsSyntaxTokens":true},"linkedEditingRange":{"dynamicRegistration":true},"typeHierarchy":{"dynamicRegistration":true},"inlineValue":{"dynamicRegistration":true},"inlayHint":{"dynamicRegistration":true,"resolveSupport":{"properties":["tooltip","textEdits","label.tooltip","label.location","label.command"]}},"diagnostic":{"dynamicRegistration":true,"relatedDocumentSupport":false}},"window":{"showMessage":{"messageActionItem":{"additionalPropertiesSupport":true}},"showDocument":{"support":true},"workDoneProgress":true},"general":{"staleRequestSupport":{"cancel":true,"retryOnContentModified":["textDocument/semanticTokens/full","textDocument/semanticTokens/range","textDocument/semanticTokens/full/delta"]},"regularExpressions":{"engine":"ECMAScript","version":"ES2020"},"markdown":{"parser":"marked","version":"1.1.0"},"positionEncodings":["utf-16"]},"notebookDocument":{"synchronization":{"dynamicRegistration":true,"executionSummarySupport":true}}},"initializationOptions":{"usePlaceholders":true,"completionDocumentation":true,"verboseOutput":false,"build.directoryFilters":["-foof","-internal/lsp/protocol/typescript"],"codelenses":{"reference":true,"gc_details":true},"analyses":{"fillstruct":true,"staticcheck":true,"unusedparams":false,"composites":false},"semanticTokens":true,"noSemanticString":true,"noSemanticNumber":true,"templateExtensions":["tmpl","gotmpl"],"ui.completion.matcher":"Fuzzy","ui.inlayhint.hints":{"assignVariableTypes":false,"compositeLiteralFields":false,"compositeLiteralTypes":false,"constantValues":false,"functionTypeParameters":false,"parameterNames":false,"rangeVariableTypes":false},"ui.vulncheck":"Off","allExperiments":true},"trace":"off","workspaceFolders":[{"uri":"file:///Users/pjw/hakim","name":"hakim"}]}`
+const input = `{"processId":46408,"clientInfo":{"name":"Visual Studio Code - Insiders","version":"1.76.0-insider"},"locale":"en-us","rootPath":"/Users/pjw/hakim","rootUri":"file:///Users/pjw/hakim","capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional","normalizesLineEndings":true,"changeAnnotationSupport":{"groupsOnLabel":true}},"configuration":true,"didChangeWatchedFiles":{"dynamicRegistration":true,"relativePatternSupport":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]},"resolveSupport":{"properties":["location.range"]}},"codeLens":{"refreshSupport":true},"executeCommand":{"dynamicRegistration":true},"didChangeConfiguration":{"dynamicRegistration":true},"workspaceFolders":true,"semanticTokens":{"refreshSupport":true},"fileOperations":{"dynamicRegistration":true,"didCreate":true,"didRename":true,"didDelete":true,"willCreate":true,"willRename":true,"willDelete":true},"inlineValue":{"refreshSupport":true},"inlayHint":{"refreshSupport":true},"diagnostics":{"refreshSupport":true}},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"codeDescriptionSupport":true,"dataSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true,"resolveSupport":{"properties":["documentation","detail","additionalTextEdits"]},"insertTextModeSupport":{"valueSet":[1,2]},"labelDetailsSupport":true},"insertTextMode":2,"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]},"completionList":{"itemDefaults":["commitCharacters","editRange","insertTextFormat","insertTextMode"]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true},"activeParameterSupport":true},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]},"labelSupport":true},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"disabledSupport":true,"dataSupport":true,"resolveSupport":{"properties":["edit"]},"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"honorsChangeAnnotations":false},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true,"prepareSupportDefaultBehavior":1,"honorsChangeAnnotations":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true,"foldingRangeKind":{"valueSet":["comment","imports","region"]},"foldingRange":{"collapsedText":false}},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"callHierarchy":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator","decorator"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"formats":["relative"],"requests":{"range":true,"full":{"delta":true}},"multilineTokenSupport":false,"overlappingTokenSupport":false,"serverCancelSupport":true,"augmentsSyntaxTokens":true},"linkedEditingRange":{"dynamicRegistration":true},"typeHierarchy":{"dynamicRegistration":true},"inlineValue":{"dynamicRegistration":true},"inlayHint":{"dynamicRegistration":true,"resolveSupport":{"properties":["tooltip","textEdits","label.tooltip","label.location","label.command"]}},"diagnostic":{"dynamicRegistration":true,"relatedDocumentSupport":false}},"window":{"showMessage":{"messageActionItem":{"additionalPropertiesSupport":true}},"showDocument":{"support":true},"workDoneProgress":true},"general":{"staleRequestSupport":{"cancel":true,"retryOnContentModified":["textDocument/semanticTokens/full","textDocument/semanticTokens/range","textDocument/semanticTokens/full/delta"]},"regularExpressions":{"engine":"ECMAScript","version":"ES2020"},"markdown":{"parser":"marked","version":"1.1.0"},"positionEncodings":["utf-16"]},"notebookDocument":{"synchronization":{"dynamicRegistration":true,"executionSummarySupport":true}}},"initializationOptions":{"usePlaceholders":true,"completionDocumentation":true,"verboseOutput":false,"build.directoryFilters":["-foof","-internal/protocol/typescript"],"codelenses":{"reference":true,"gc_details":true},"analyses":{"fillstruct":true,"staticcheck":true,"unusedparams":false,"composites":false},"semanticTokens":true,"noSemanticString":true,"noSemanticNumber":true,"templateExtensions":["tmpl","gotmpl"],"ui.completion.matcher":"Fuzzy","ui.inlayhint.hints":{"assignVariableTypes":false,"compositeLiteralFields":false,"compositeLiteralTypes":false,"constantValues":false,"functionTypeParameters":false,"parameterNames":false,"rangeVariableTypes":false},"ui.vulncheck":"Off","allExperiments":true},"trace":"off","workspaceFolders":[{"uri":"file:///Users/pjw/hakim","name":"hakim"}]}`
type DiffReporter struct {
path cmp.Path
diff --git a/gopls/internal/lsp/protocol/log.go b/gopls/internal/protocol/log.go
similarity index 100%
rename from gopls/internal/lsp/protocol/log.go
rename to gopls/internal/protocol/log.go
diff --git a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/protocol/mapper.go
similarity index 100%
rename from gopls/internal/lsp/protocol/mapper.go
rename to gopls/internal/protocol/mapper.go
diff --git a/gopls/internal/lsp/protocol/mapper_test.go b/gopls/internal/protocol/mapper_test.go
similarity index 99%
rename from gopls/internal/lsp/protocol/mapper_test.go
rename to gopls/internal/protocol/mapper_test.go
index 77257691725..8ba611a99f9 100644
--- a/gopls/internal/lsp/protocol/mapper_test.go
+++ b/gopls/internal/protocol/mapper_test.go
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// This file tests Mapper's logic for converting between offsets,
diff --git a/gopls/internal/lsp/protocol/protocol.go b/gopls/internal/protocol/protocol.go
similarity index 91%
rename from gopls/internal/lsp/protocol/protocol.go
rename to gopls/internal/protocol/protocol.go
index 3ece42b7a11..09bfaca2e86 100644
--- a/gopls/internal/lsp/protocol/protocol.go
+++ b/gopls/internal/protocol/protocol.go
@@ -11,6 +11,8 @@ import (
"fmt"
"io"
+ "golang.org/x/tools/gopls/internal/telemetry"
+ "golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/jsonrpc2"
jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2"
@@ -241,7 +243,7 @@ func CancelHandler(handler jsonrpc2.Handler) jsonrpc2.Handler {
return handler(ctx, replyWithDetachedContext, req)
}
var params CancelParams
- if err := unmarshalParams(req.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(req.Params(), ¶ms); err != nil {
return sendParseError(ctx, reply, err)
}
if n, ok := params.ID.(float64); ok {
@@ -271,16 +273,14 @@ func cancelCall(ctx context.Context, sender connSender, id jsonrpc2.ID) {
sender.Notify(ctx, "$/cancelRequest", &CancelParams{ID: &id})
}
-// unmarshalParams unmarshals msg into the variable pointed to by
-// params. In JSONRPC, request.params is optional, so msg may may be
+// UnmarshalJSON unmarshals msg into the variable pointed to by
+// params. In JSONRPC, optional messages may be
// "null", in which case it is a no-op.
-func unmarshalParams(msg json.RawMessage, params any) error {
- if len(msg) > 0 && !bytes.Equal(msg, []byte("null")) {
- if err := json.Unmarshal(msg, params); err != nil {
- return err
- }
+func UnmarshalJSON(msg json.RawMessage, v any) error {
+ if len(msg) == 0 || bytes.Equal(msg, []byte("null")) {
+ return nil
}
- return nil
+ return json.Unmarshal(msg, v)
}
func sendParseError(ctx context.Context, reply jsonrpc2.Replier, err error) error {
@@ -297,3 +297,17 @@ func NonNilSlice[T comparable](x []T) []T {
}
return x
}
+
+func recoverHandlerPanic(method string) {
+ // Report panics in the handler goroutine,
+ // unless we have enabled the monitor,
+ // which reports all crashes.
+ if !telemetry.CrashMonitorSupported() {
+ defer func() {
+ if x := recover(); x != nil {
+ bug.Reportf("panic in %s request", method)
+ panic(x)
+ }
+ }()
+ }
+}
diff --git a/gopls/internal/lsp/protocol/semantic.go b/gopls/internal/protocol/semantic.go
similarity index 100%
rename from gopls/internal/lsp/protocol/semantic.go
rename to gopls/internal/protocol/semantic.go
diff --git a/gopls/internal/protocol/semtok/semtok.go b/gopls/internal/protocol/semtok/semtok.go
new file mode 100644
index 00000000000..e69ec825f17
--- /dev/null
+++ b/gopls/internal/protocol/semtok/semtok.go
@@ -0,0 +1,101 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The semtok package provides an encoder for LSP's semantic tokens.
+package semtok
+
+import "sort"
+
+// A Token provides the extent and semantics of a token.
+type Token struct {
+ Line, Start uint32
+ Len uint32
+ Type TokenType
+ Modifiers []string
+}
+
+type TokenType string
+
+const (
+ TokNamespace TokenType = "namespace"
+ TokType TokenType = "type"
+ TokInterface TokenType = "interface"
+ TokTypeParam TokenType = "typeParameter"
+ TokParameter TokenType = "parameter"
+ TokVariable TokenType = "variable"
+ TokMethod TokenType = "method"
+ TokFunction TokenType = "function"
+ TokKeyword TokenType = "keyword"
+ TokComment TokenType = "comment"
+ TokString TokenType = "string"
+ TokNumber TokenType = "number"
+ TokOperator TokenType = "operator"
+ TokMacro TokenType = "macro" // for templates
+)
+
+// Encode returns the LSP encoding of a sequence of tokens.
+// The noStrings, noNumbers options cause strings, numbers to be skipped.
+// The lists of types and modifiers determines the bitfield encoding.
+func Encode(
+ tokens []Token,
+ noStrings, noNumbers bool,
+ types, modifiers []string) []uint32 {
+
+ // binary operators, at least, will be out of order
+ sort.Slice(tokens, func(i, j int) bool {
+ if tokens[i].Line != tokens[j].Line {
+ return tokens[i].Line < tokens[j].Line
+ }
+ return tokens[i].Start < tokens[j].Start
+ })
+
+ typeMap := make(map[TokenType]int)
+ for i, t := range types {
+ typeMap[TokenType(t)] = i
+ }
+
+ modMap := make(map[string]int)
+ for i, m := range modifiers {
+ modMap[m] = 1 << uint(i) // go 1.12 compatibility
+ }
+
+ // each semantic token needs five values
+ // (see Integer Encoding for Tokens in the LSP spec)
+ x := make([]uint32, 5*len(tokens))
+ var j int
+ var last Token
+ for i := 0; i < len(tokens); i++ {
+ item := tokens[i]
+ typ, ok := typeMap[item.Type]
+ if !ok {
+ continue // client doesn't want typeStr
+ }
+ if item.Type == TokString && noStrings {
+ continue
+ }
+ if item.Type == TokNumber && noNumbers {
+ continue
+ }
+ if j == 0 {
+ x[0] = tokens[0].Line
+ } else {
+ x[j] = item.Line - last.Line
+ }
+ x[j+1] = item.Start
+ if j > 0 && x[j] == 0 {
+ x[j+1] = item.Start - last.Start
+ }
+ x[j+2] = item.Len
+ x[j+3] = uint32(typ)
+ mask := 0
+ for _, s := range item.Modifiers {
+ // modMap[s] is 0 if the client doesn't want this modifier
+ mask |= modMap[s]
+ }
+ x[j+4] = uint32(mask)
+ j += 5
+ last = item
+ }
+ return x[:j]
+}
diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/protocol/span.go
similarity index 100%
rename from gopls/internal/lsp/protocol/span.go
rename to gopls/internal/protocol/span.go
diff --git a/gopls/internal/lsp/protocol/tsclient.go b/gopls/internal/protocol/tsclient.go
similarity index 91%
rename from gopls/internal/lsp/protocol/tsclient.go
rename to gopls/internal/protocol/tsclient.go
index 6ea20f036ee..651c6796fb0 100644
--- a/gopls/internal/lsp/protocol/tsclient.go
+++ b/gopls/internal/protocol/tsclient.go
@@ -13,7 +13,6 @@ package protocol
import (
"context"
- "golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/jsonrpc2"
)
@@ -41,16 +40,11 @@ type Client interface {
}
func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) {
- defer func() {
- if x := recover(); x != nil {
- bug.Reportf("client panic in %s request", r.Method())
- panic(x)
- }
- }()
+ defer recoverHandlerPanic(r.Method())
switch r.Method() {
case "$/logTrace":
var params LogTraceParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.LogTrace(ctx, ¶ms)
@@ -58,7 +52,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "$/progress":
var params ProgressParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.Progress(ctx, ¶ms)
@@ -66,7 +60,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "client/registerCapability":
var params RegistrationParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.RegisterCapability(ctx, ¶ms)
@@ -74,7 +68,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "client/unregisterCapability":
var params UnregistrationParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.UnregisterCapability(ctx, ¶ms)
@@ -82,7 +76,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "telemetry/event":
var params interface{}
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.Event(ctx, ¶ms)
@@ -90,7 +84,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "textDocument/publishDiagnostics":
var params PublishDiagnosticsParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.PublishDiagnostics(ctx, ¶ms)
@@ -98,7 +92,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "window/logMessage":
var params LogMessageParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.LogMessage(ctx, ¶ms)
@@ -106,7 +100,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "window/showDocument":
var params ShowDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := client.ShowDocument(ctx, ¶ms)
@@ -117,7 +111,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "window/showMessage":
var params ShowMessageParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.ShowMessage(ctx, ¶ms)
@@ -125,7 +119,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "window/showMessageRequest":
var params ShowMessageRequestParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := client.ShowMessageRequest(ctx, ¶ms)
@@ -136,7 +130,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "window/workDoneProgress/create":
var params WorkDoneProgressCreateParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := client.WorkDoneProgressCreate(ctx, ¶ms)
@@ -144,7 +138,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "workspace/applyEdit":
var params ApplyWorkspaceEditParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := client.ApplyEdit(ctx, ¶ms)
@@ -159,7 +153,7 @@ func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier,
case "workspace/configuration":
var params ParamConfiguration
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := client.Configuration(ctx, ¶ms)
diff --git a/gopls/internal/lsp/protocol/tsdocument_changes.go b/gopls/internal/protocol/tsdocument_changes.go
similarity index 100%
rename from gopls/internal/lsp/protocol/tsdocument_changes.go
rename to gopls/internal/protocol/tsdocument_changes.go
diff --git a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/protocol/tsjson.go
similarity index 99%
rename from gopls/internal/lsp/protocol/tsjson.go
rename to gopls/internal/protocol/tsjson.go
index f069e0e4157..c19c43557d6 100644
--- a/gopls/internal/lsp/protocol/tsjson.go
+++ b/gopls/internal/protocol/tsjson.go
@@ -473,6 +473,36 @@ func (t *Or_DocumentFilter) UnmarshalJSON(x []byte) error {
return &UnmarshalError{"unmarshal failed to match one of [NotebookCellTextDocumentFilter TextDocumentFilter]"}
}
+func (t Or_GlobPattern) MarshalJSON() ([]byte, error) {
+ switch x := t.Value.(type) {
+ case Pattern:
+ return json.Marshal(x)
+ case RelativePattern:
+ return json.Marshal(x)
+ case nil:
+ return []byte("null"), nil
+ }
+ return nil, fmt.Errorf("type %T not one of [Pattern RelativePattern]", t)
+}
+
+func (t *Or_GlobPattern) UnmarshalJSON(x []byte) error {
+ if string(x) == "null" {
+ t.Value = nil
+ return nil
+ }
+ var h0 Pattern
+ if err := json.Unmarshal(x, &h0); err == nil {
+ t.Value = h0
+ return nil
+ }
+ var h1 RelativePattern
+ if err := json.Unmarshal(x, &h1); err == nil {
+ t.Value = h1
+ return nil
+ }
+ return &UnmarshalError{"unmarshal failed to match one of [Pattern RelativePattern]"}
+}
+
func (t Or_Hover_contents) MarshalJSON() ([]byte, error) {
switch x := t.Value.(type) {
case MarkedString:
@@ -854,36 +884,6 @@ func (t *Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) Unm
return &UnmarshalError{"unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]"}
}
-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 &UnmarshalError{"unmarshal failed to match one of [URI WorkspaceFolder]"}
-}
-
func (t Or_Result_textDocument_codeAction_Item0_Elem) MarshalJSON() ([]byte, error) {
switch x := t.Value.(type) {
case CodeAction:
diff --git a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/protocol/tsprotocol.go
similarity index 99%
rename from gopls/internal/lsp/protocol/tsprotocol.go
rename to gopls/internal/protocol/tsprotocol.go
index 48adb18afca..7994e667952 100644
--- a/gopls/internal/lsp/protocol/tsprotocol.go
+++ b/gopls/internal/protocol/tsprotocol.go
@@ -489,7 +489,7 @@ type CodeAction struct {
// a `textDocument/codeAction` and a `codeAction/resolve` request.
//
// @since 3.16.0
- Data interface{} `json:"data,omitempty"`
+ Data *json.RawMessage `json:"data,omitempty"`
}
// The Client Capabilities of a {@link CodeActionRequest}.
@@ -2121,7 +2121,7 @@ type GeneralClientCapabilities struct {
// The glob pattern. Either a string pattern or a relative pattern.
//
// @since 3.17.0
-type GlobPattern = string // (alias)
+type GlobPattern = Or_GlobPattern // (alias)
// The result of a hover request.
type Hover struct {
// The hover's content
@@ -3108,6 +3108,11 @@ type Or_DocumentFilter struct {
Value interface{} `json:"value"`
}
+// created for Or [Pattern RelativePattern]
+type Or_GlobPattern struct {
+ Value interface{} `json:"value"`
+}
+
// created for Or [MarkedString MarkupContent []MarkedString]
type Or_Hover_contents struct {
Value interface{} `json:"value"`
@@ -3168,11 +3173,6 @@ type Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value struct {
Value interface{} `json:"value"`
}
-// created for Or [URI WorkspaceFolder]
-type Or_RelativePattern_baseUri struct {
- Value interface{} `json:"value"`
-}
-
// created for Or [CodeAction Command]
type Or_Result_textDocument_codeAction_Item0_Elem struct {
Value interface{} `json:"value"`
@@ -3656,7 +3656,7 @@ type RelatedUnchangedDocumentDiagnosticReport struct {
type RelativePattern struct {
// A workspace folder or a base URI to which this pattern will be matched
// against relatively.
- BaseURI Or_RelativePattern_baseUri `json:"baseUri"`
+ BaseURI DocumentURI `json:"baseUri"`
// The actual glob pattern;
Pattern Pattern `json:"pattern"`
}
diff --git a/gopls/internal/lsp/protocol/tsserver.go b/gopls/internal/protocol/tsserver.go
similarity index 90%
rename from gopls/internal/lsp/protocol/tsserver.go
rename to gopls/internal/protocol/tsserver.go
index a9282768e66..f667da4e490 100644
--- a/gopls/internal/lsp/protocol/tsserver.go
+++ b/gopls/internal/protocol/tsserver.go
@@ -13,7 +13,6 @@ package protocol
import (
"context"
- "golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/jsonrpc2"
)
@@ -95,16 +94,11 @@ type Server interface {
}
func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) {
- defer func() {
- if x := recover(); x != nil {
- bug.Reportf("server panic in %s request", r.Method())
- panic(x)
- }
- }()
+ defer recoverHandlerPanic(r.Method())
switch r.Method() {
case "$/progress":
var params ProgressParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.Progress(ctx, ¶ms)
@@ -112,7 +106,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "$/setTrace":
var params SetTraceParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.SetTrace(ctx, ¶ms)
@@ -120,7 +114,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "callHierarchy/incomingCalls":
var params CallHierarchyIncomingCallsParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.IncomingCalls(ctx, ¶ms)
@@ -131,7 +125,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "callHierarchy/outgoingCalls":
var params CallHierarchyOutgoingCallsParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.OutgoingCalls(ctx, ¶ms)
@@ -142,7 +136,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "codeAction/resolve":
var params CodeAction
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.ResolveCodeAction(ctx, ¶ms)
@@ -153,7 +147,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "codeLens/resolve":
var params CodeLens
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.ResolveCodeLens(ctx, ¶ms)
@@ -164,7 +158,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "completionItem/resolve":
var params CompletionItem
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.ResolveCompletionItem(ctx, ¶ms)
@@ -175,7 +169,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "documentLink/resolve":
var params DocumentLink
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.ResolveDocumentLink(ctx, ¶ms)
@@ -190,7 +184,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "initialize":
var params ParamInitialize
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Initialize(ctx, ¶ms)
@@ -201,7 +195,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "initialized":
var params InitializedParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.Initialized(ctx, ¶ms)
@@ -209,7 +203,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "inlayHint/resolve":
var params InlayHint
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Resolve(ctx, ¶ms)
@@ -220,7 +214,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "notebookDocument/didChange":
var params DidChangeNotebookDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidChangeNotebookDocument(ctx, ¶ms)
@@ -228,7 +222,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "notebookDocument/didClose":
var params DidCloseNotebookDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidCloseNotebookDocument(ctx, ¶ms)
@@ -236,7 +230,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "notebookDocument/didOpen":
var params DidOpenNotebookDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidOpenNotebookDocument(ctx, ¶ms)
@@ -244,7 +238,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "notebookDocument/didSave":
var params DidSaveNotebookDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidSaveNotebookDocument(ctx, ¶ms)
@@ -256,7 +250,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/codeAction":
var params CodeActionParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.CodeAction(ctx, ¶ms)
@@ -267,7 +261,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/codeLens":
var params CodeLensParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.CodeLens(ctx, ¶ms)
@@ -278,7 +272,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/colorPresentation":
var params ColorPresentationParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.ColorPresentation(ctx, ¶ms)
@@ -289,7 +283,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/completion":
var params CompletionParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Completion(ctx, ¶ms)
@@ -300,7 +294,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/declaration":
var params DeclarationParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Declaration(ctx, ¶ms)
@@ -311,7 +305,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/definition":
var params DefinitionParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Definition(ctx, ¶ms)
@@ -322,7 +316,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/diagnostic":
var params string
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Diagnostic(ctx, ¶ms)
@@ -333,7 +327,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/didChange":
var params DidChangeTextDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidChange(ctx, ¶ms)
@@ -341,7 +335,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/didClose":
var params DidCloseTextDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidClose(ctx, ¶ms)
@@ -349,7 +343,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/didOpen":
var params DidOpenTextDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidOpen(ctx, ¶ms)
@@ -357,7 +351,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/didSave":
var params DidSaveTextDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidSave(ctx, ¶ms)
@@ -365,7 +359,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/documentColor":
var params DocumentColorParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.DocumentColor(ctx, ¶ms)
@@ -376,7 +370,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/documentHighlight":
var params DocumentHighlightParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.DocumentHighlight(ctx, ¶ms)
@@ -387,7 +381,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/documentLink":
var params DocumentLinkParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.DocumentLink(ctx, ¶ms)
@@ -398,7 +392,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/documentSymbol":
var params DocumentSymbolParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.DocumentSymbol(ctx, ¶ms)
@@ -409,7 +403,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/foldingRange":
var params FoldingRangeParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.FoldingRange(ctx, ¶ms)
@@ -420,7 +414,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/formatting":
var params DocumentFormattingParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Formatting(ctx, ¶ms)
@@ -431,7 +425,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/hover":
var params HoverParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Hover(ctx, ¶ms)
@@ -442,7 +436,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/implementation":
var params ImplementationParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Implementation(ctx, ¶ms)
@@ -453,7 +447,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/inlayHint":
var params InlayHintParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.InlayHint(ctx, ¶ms)
@@ -464,7 +458,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/inlineCompletion":
var params InlineCompletionParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.InlineCompletion(ctx, ¶ms)
@@ -475,7 +469,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/inlineValue":
var params InlineValueParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.InlineValue(ctx, ¶ms)
@@ -486,7 +480,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/linkedEditingRange":
var params LinkedEditingRangeParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.LinkedEditingRange(ctx, ¶ms)
@@ -497,7 +491,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/moniker":
var params MonikerParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Moniker(ctx, ¶ms)
@@ -508,7 +502,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/onTypeFormatting":
var params DocumentOnTypeFormattingParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.OnTypeFormatting(ctx, ¶ms)
@@ -519,7 +513,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/prepareCallHierarchy":
var params CallHierarchyPrepareParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.PrepareCallHierarchy(ctx, ¶ms)
@@ -530,7 +524,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/prepareRename":
var params PrepareRenameParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.PrepareRename(ctx, ¶ms)
@@ -541,7 +535,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/prepareTypeHierarchy":
var params TypeHierarchyPrepareParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.PrepareTypeHierarchy(ctx, ¶ms)
@@ -552,7 +546,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/rangeFormatting":
var params DocumentRangeFormattingParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.RangeFormatting(ctx, ¶ms)
@@ -563,7 +557,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/rangesFormatting":
var params DocumentRangesFormattingParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.RangesFormatting(ctx, ¶ms)
@@ -574,7 +568,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/references":
var params ReferenceParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.References(ctx, ¶ms)
@@ -585,7 +579,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/rename":
var params RenameParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Rename(ctx, ¶ms)
@@ -596,7 +590,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/selectionRange":
var params SelectionRangeParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.SelectionRange(ctx, ¶ms)
@@ -607,7 +601,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/semanticTokens/full":
var params SemanticTokensParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.SemanticTokensFull(ctx, ¶ms)
@@ -618,7 +612,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/semanticTokens/full/delta":
var params SemanticTokensDeltaParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.SemanticTokensFullDelta(ctx, ¶ms)
@@ -629,7 +623,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/semanticTokens/range":
var params SemanticTokensRangeParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.SemanticTokensRange(ctx, ¶ms)
@@ -640,7 +634,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/signatureHelp":
var params SignatureHelpParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.SignatureHelp(ctx, ¶ms)
@@ -651,7 +645,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/typeDefinition":
var params TypeDefinitionParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.TypeDefinition(ctx, ¶ms)
@@ -662,7 +656,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/willSave":
var params WillSaveTextDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.WillSave(ctx, ¶ms)
@@ -670,7 +664,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "textDocument/willSaveWaitUntil":
var params WillSaveTextDocumentParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.WillSaveWaitUntil(ctx, ¶ms)
@@ -681,7 +675,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "typeHierarchy/subtypes":
var params TypeHierarchySubtypesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Subtypes(ctx, ¶ms)
@@ -692,7 +686,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "typeHierarchy/supertypes":
var params TypeHierarchySupertypesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Supertypes(ctx, ¶ms)
@@ -703,7 +697,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "window/workDoneProgress/cancel":
var params WorkDoneProgressCancelParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.WorkDoneProgressCancel(ctx, ¶ms)
@@ -711,7 +705,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/diagnostic":
var params WorkspaceDiagnosticParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.DiagnosticWorkspace(ctx, ¶ms)
@@ -722,7 +716,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/didChangeConfiguration":
var params DidChangeConfigurationParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidChangeConfiguration(ctx, ¶ms)
@@ -730,7 +724,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/didChangeWatchedFiles":
var params DidChangeWatchedFilesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidChangeWatchedFiles(ctx, ¶ms)
@@ -738,7 +732,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/didChangeWorkspaceFolders":
var params DidChangeWorkspaceFoldersParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidChangeWorkspaceFolders(ctx, ¶ms)
@@ -746,7 +740,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/didCreateFiles":
var params CreateFilesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidCreateFiles(ctx, ¶ms)
@@ -754,7 +748,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/didDeleteFiles":
var params DeleteFilesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidDeleteFiles(ctx, ¶ms)
@@ -762,7 +756,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/didRenameFiles":
var params RenameFilesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
err := server.DidRenameFiles(ctx, ¶ms)
@@ -770,7 +764,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/executeCommand":
var params ExecuteCommandParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.ExecuteCommand(ctx, ¶ms)
@@ -781,7 +775,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/symbol":
var params WorkspaceSymbolParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.Symbol(ctx, ¶ms)
@@ -792,7 +786,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/willCreateFiles":
var params CreateFilesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.WillCreateFiles(ctx, ¶ms)
@@ -803,7 +797,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/willDeleteFiles":
var params DeleteFilesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.WillDeleteFiles(ctx, ¶ms)
@@ -814,7 +808,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspace/willRenameFiles":
var params RenameFilesParams
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.WillRenameFiles(ctx, ¶ms)
@@ -825,7 +819,7 @@ func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier,
case "workspaceSymbol/resolve":
var params WorkspaceSymbol
- if err := unmarshalParams(r.Params(), ¶ms); err != nil {
+ if err := UnmarshalJSON(r.Params(), ¶ms); err != nil {
return true, sendParseError(ctx, reply, err)
}
resp, err := server.ResolveWorkspaceSymbol(ctx, ¶ms)
diff --git a/gopls/internal/lsp/protocol/uri.go b/gopls/internal/protocol/uri.go
similarity index 100%
rename from gopls/internal/lsp/protocol/uri.go
rename to gopls/internal/protocol/uri.go
diff --git a/gopls/internal/lsp/protocol/uri_test.go b/gopls/internal/protocol/uri_test.go
similarity index 98%
rename from gopls/internal/lsp/protocol/uri_test.go
rename to gopls/internal/protocol/uri_test.go
index 3c1d7e598f1..cad71ddc13c 100644
--- a/gopls/internal/lsp/protocol/uri_test.go
+++ b/gopls/internal/protocol/uri_test.go
@@ -10,7 +10,7 @@ package protocol_test
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// TestURIFromPath tests the conversion between URIs and filenames. The test cases
diff --git a/gopls/internal/lsp/protocol/uri_windows_test.go b/gopls/internal/protocol/uri_windows_test.go
similarity index 86%
rename from gopls/internal/lsp/protocol/uri_windows_test.go
rename to gopls/internal/protocol/uri_windows_test.go
index e607468a322..08471167a22 100644
--- a/gopls/internal/lsp/protocol/uri_windows_test.go
+++ b/gopls/internal/protocol/uri_windows_test.go
@@ -8,9 +8,10 @@
package protocol_test
import (
+ "path/filepath"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// TestURIFromPath tests the conversion between URIs and filenames. The test cases
@@ -18,6 +19,15 @@ import (
// tests by using only forward slashes, assuming that the standard library
// functions filepath.ToSlash and filepath.FromSlash do not need testing.
func TestURIFromPath(t *testing.T) {
+ rootPath, err := filepath.Abs("/")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(rootPath) < 2 || rootPath[1] != ':' {
+ t.Fatalf("malformed root path %q", rootPath)
+ }
+ driveLetter := string(rootPath[0])
+
for _, test := range []struct {
path, wantFile string
wantURI protocol.DocumentURI
@@ -44,13 +54,13 @@ func TestURIFromPath(t *testing.T) {
},
{
path: `\path\to\dir`,
- wantFile: `C:\path\to\dir`,
- wantURI: protocol.DocumentURI("file:///C:/path/to/dir"),
+ wantFile: driveLetter + `:\path\to\dir`,
+ wantURI: protocol.DocumentURI("file:///" + driveLetter + ":/path/to/dir"),
},
{
path: `\a\b\c\src\bob.go`,
- wantFile: `C:\a\b\c\src\bob.go`,
- wantURI: protocol.DocumentURI("file:///C:/a/b/c/src/bob.go"),
+ wantFile: driveLetter + `:\a\b\c\src\bob.go`,
+ wantURI: protocol.DocumentURI("file:///" + driveLetter + ":/a/b/c/src/bob.go"),
},
{
path: `c:\Go\src\bob george\george\george.go`,
diff --git a/gopls/internal/server/call_hierarchy.go b/gopls/internal/server/call_hierarchy.go
index 8dd1f3e3ce7..671d4f8c81c 100644
--- a/gopls/internal/server/call_hierarchy.go
+++ b/gopls/internal/server/call_hierarchy.go
@@ -8,8 +8,8 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
@@ -25,7 +25,7 @@ func (s *server) PrepareCallHierarchy(ctx context.Context, params *protocol.Call
if snapshot.FileKind(fh) != file.Go {
return nil, nil // empty result
}
- return source.PrepareCallHierarchy(ctx, snapshot, fh, params.Position)
+ return golang.PrepareCallHierarchy(ctx, snapshot, fh, params.Position)
}
func (s *server) IncomingCalls(ctx context.Context, params *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) {
@@ -40,7 +40,7 @@ func (s *server) IncomingCalls(ctx context.Context, params *protocol.CallHierarc
if snapshot.FileKind(fh) != file.Go {
return nil, nil // empty result
}
- return source.IncomingCalls(ctx, snapshot, fh, params.Item.Range.Start)
+ return golang.IncomingCalls(ctx, snapshot, fh, params.Item.Range.Start)
}
func (s *server) OutgoingCalls(ctx context.Context, params *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) {
@@ -55,5 +55,5 @@ func (s *server) OutgoingCalls(ctx context.Context, params *protocol.CallHierarc
if snapshot.FileKind(fh) != file.Go {
return nil, nil // empty result
}
- return source.OutgoingCalls(ctx, snapshot, fh, params.Item.Range.Start)
+ return golang.OutgoingCalls(ctx, snapshot, fh, params.Item.Range.Start)
}
diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go
index 4ec105fa34d..b26f9780c60 100644
--- a/gopls/internal/server/code_action.go
+++ b/gopls/internal/server/code_action.go
@@ -7,26 +7,16 @@ package server
import (
"context"
"fmt"
- "go/ast"
"sort"
"strings"
- "golang.org/x/tools/go/ast/inspector"
- "golang.org/x/tools/gopls/internal/analysis/fillstruct"
- "golang.org/x/tools/gopls/internal/analysis/infertypeargs"
- "golang.org/x/tools/gopls/internal/analysis/stubmethods"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "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/golang"
"golang.org/x/tools/gopls/internal/mod"
- "golang.org/x/tools/gopls/internal/settings"
- "golang.org/x/tools/gopls/internal/util/bug"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/internal/event"
- "golang.org/x/tools/internal/event/tag"
- "golang.org/x/tools/internal/imports"
)
func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
@@ -113,165 +103,22 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara
return actions, nil
case file.Go:
- diagnostics := params.Context.Diagnostics
-
// Don't suggest fixes for generated files, since they are generally
// not useful and some editors may apply them automatically on save.
- if source.IsGenerated(ctx, snapshot, uri) {
+ if golang.IsGenerated(ctx, snapshot, uri) {
return nil, nil
}
- actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, diagnostics, want)
+ actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, params.Context.Diagnostics, want)
if err != nil {
return nil, err
}
- // Only compute quick fixes if there are any diagnostics to fix.
- wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0
-
- // Code actions requiring syntax information alone.
- if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] {
- pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull)
- if err != nil {
- return nil, err
- }
-
- // Process any missing imports and pair them with the diagnostics they
- // fix.
- if wantQuickFixes || want[protocol.SourceOrganizeImports] {
- importEdits, importEditsPerFix, err := source.AllImportsFixes(ctx, snapshot, pgf)
- if err != nil {
- event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Path()))
- importEdits = nil
- importEditsPerFix = nil
- }
-
- // Separate this into a set of codeActions per diagnostic, where
- // each action is the addition, removal, or renaming of one import.
- if wantQuickFixes {
- for _, importFix := range importEditsPerFix {
- fixed := fixedByImportFix(importFix.Fix, diagnostics)
- if len(fixed) == 0 {
- continue
- }
- actions = append(actions, protocol.CodeAction{
- Title: importFixTitle(importFix.Fix),
- Kind: protocol.QuickFix,
- Edit: &protocol.WorkspaceEdit{
- DocumentChanges: documentChanges(fh, importFix.Edits),
- },
- Diagnostics: fixed,
- })
- }
- }
-
- // Send all of the import edits as one code action if the file is
- // being organized.
- if want[protocol.SourceOrganizeImports] && len(importEdits) > 0 {
- actions = append(actions, protocol.CodeAction{
- Title: "Organize Imports",
- Kind: protocol.SourceOrganizeImports,
- Edit: &protocol.WorkspaceEdit{
- DocumentChanges: documentChanges(fh, importEdits),
- },
- })
- }
- }
-
- if want[protocol.RefactorExtract] {
- extractions, err := refactorExtract(pgf, params.Range)
- if err != nil {
- return nil, err
- }
- actions = append(actions, extractions...)
- }
- }
-
- var stubMethodsDiagnostics []protocol.Diagnostic
- if wantQuickFixes && snapshot.Options().IsAnalyzerEnabled(stubmethods.Analyzer.Name) {
- for _, pd := range diagnostics {
- if stubmethods.MatchesMessage(pd.Message) {
- stubMethodsDiagnostics = append(stubMethodsDiagnostics, pd)
- }
- }
- }
-
- // Code actions requiring type information.
- if len(stubMethodsDiagnostics) > 0 ||
- want[protocol.RefactorRewrite] ||
- want[protocol.RefactorInline] ||
- want[protocol.GoTest] {
- pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI())
- if err != nil {
- return nil, err
- }
- for _, pd := range stubMethodsDiagnostics {
- start, end, err := pgf.RangePos(pd.Range)
- if err != nil {
- return nil, err
- }
- action, ok, err := func() (_ protocol.CodeAction, _ bool, rerr error) {
- // golang/go#61693: code actions were refactored to run outside of the
- // analysis framework, but as a result they lost their panic recovery.
- //
- // Stubmethods "should never fail"", but put back the panic recovery as a
- // defensive measure.
- defer func() {
- if r := recover(); r != nil {
- rerr = bug.Errorf("stubmethods panicked: %v", r)
- }
- }()
- d, ok := stubmethods.DiagnosticForError(pkg.FileSet(), pgf.File, start, end, pd.Message, pkg.GetTypesInfo())
- if !ok {
- return protocol.CodeAction{}, false, nil
- }
- cmd, err := command.NewApplyFixCommand(d.Message, command.ApplyFixArgs{
- URI: pgf.URI,
- Fix: string(settings.StubMethods),
- Range: pd.Range,
- })
- if err != nil {
- return protocol.CodeAction{}, false, err
- }
- return protocol.CodeAction{
- Title: d.Message,
- Kind: protocol.QuickFix,
- Command: &cmd,
- Diagnostics: []protocol.Diagnostic{pd},
- }, true, nil
- }()
- if err != nil {
- return nil, err
- }
- if ok {
- actions = append(actions, action)
- }
- }
-
- if want[protocol.RefactorRewrite] {
- rewrites, err := refactorRewrite(snapshot, pkg, pgf, fh, params.Range)
- if err != nil {
- return nil, err
- }
- actions = append(actions, rewrites...)
- }
-
- if want[protocol.RefactorInline] {
- rewrites, err := refactorInline(pkg, pgf, params.Range)
- if err != nil {
- return nil, err
- }
- actions = append(actions, rewrites...)
- }
-
- if want[protocol.GoTest] {
- fixes, err := goTest(pkg, pgf, params.Range)
- if err != nil {
- return nil, err
- }
- actions = append(actions, fixes...)
- }
+ moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, want)
+ if err != nil {
+ return nil, err
}
+ actions = append(actions, moreActions...)
return actions, nil
@@ -281,356 +128,53 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara
}
}
-func (s *server) findMatchingDiagnostics(uri protocol.DocumentURI, pd protocol.Diagnostic) []*cache.Diagnostic {
- s.diagnosticsMu.Lock()
- defer s.diagnosticsMu.Unlock()
-
- var sds []*cache.Diagnostic
- for _, viewDiags := range s.diagnostics[uri].byView {
- for _, sd := range viewDiags.diagnostics {
- sameDiagnostic := (pd.Message == strings.TrimSpace(sd.Message) && // extra space may have been trimmed when converting to protocol.Diagnostic
- protocol.CompareRange(pd.Range, sd.Range) == 0 &&
- pd.Source == string(sd.Source))
-
- if sameDiagnostic {
- sds = append(sds, sd)
- }
- }
- }
- return sds
-}
-
-func (s *server) getSupportedCodeActions() []protocol.CodeActionKind {
- allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
- for _, kinds := range s.Options().SupportedCodeActions {
- for kind := range kinds {
- allCodeActionKinds[kind] = struct{}{}
- }
- }
- var result []protocol.CodeActionKind
- for kind := range allCodeActionKinds {
- result = append(result, kind)
- }
- sort.Slice(result, func(i, j int) bool {
- return result[i] < result[j]
- })
- return result
-}
-
-func importFixTitle(fix *imports.ImportFix) string {
- var str string
- switch fix.FixType {
- case imports.AddImport:
- str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
- case imports.DeleteImport:
- str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
- case imports.SetImportName:
- str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath)
- }
- return str
-}
-
-// fixedByImportFix filters the provided slice of diagnostics to those that
-// would be fixed by the provided imports fix.
-func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) []protocol.Diagnostic {
- var results []protocol.Diagnostic
- for _, diagnostic := range diagnostics {
- switch {
- // "undeclared name: X" may be an unresolved import.
- case strings.HasPrefix(diagnostic.Message, "undeclared name: "):
- ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ")
- 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: ")
- if ident == fix.IdentName {
- results = append(results, diagnostic)
- }
- // "X imported but not used" is an unused import.
- // "X imported but not used as Y" is an unused import.
- case strings.Contains(diagnostic.Message, " imported but not used"):
- idx := strings.Index(diagnostic.Message, " imported but not used")
- importPath := diagnostic.Message[:idx]
- if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) {
- results = append(results, diagnostic)
- }
- }
- }
- return results
-}
-
-func refactorExtract(pgf *source.ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) {
- if rng.Start == rng.End {
- return nil, nil
- }
+// ResolveCodeAction resolves missing Edit information (that is, computes the
+// details of the necessary patch) in the given code action using the provided
+// Data field of the CodeAction, which should contain the raw json of a protocol.Command.
+//
+// This should be called by the client before applying code actions, when the
+// client has code action resolve support.
+//
+// This feature allows capable clients to preview and selectively apply the diff
+// instead of applying the whole thing unconditionally through workspace/applyEdit.
+func (s *server) ResolveCodeAction(ctx context.Context, ca *protocol.CodeAction) (*protocol.CodeAction, error) {
+ ctx, done := event.Start(ctx, "lsp.Server.resolveCodeAction")
+ defer done()
- start, end, err := pgf.RangePos(rng)
- if err != nil {
- return nil, err
- }
- puri := pgf.URI
- var commands []protocol.Command
- if _, ok, methodOk, _ := source.CanExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok {
- cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{
- URI: puri,
- Fix: string(settings.ExtractFunction),
- Range: rng,
- })
- if err != nil {
+ // Only resolve the code action if there is Data provided.
+ var cmd protocol.Command
+ if ca.Data != nil {
+ if err := protocol.UnmarshalJSON(*ca.Data, &cmd); err != nil {
return nil, err
}
- commands = append(commands, cmd)
- if methodOk {
- cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{
- URI: puri,
- Fix: string(settings.ExtractMethod),
- Range: rng,
- })
- if err != nil {
- return nil, err
- }
- commands = append(commands, cmd)
- }
}
- if _, _, ok, _ := source.CanExtractVariable(start, end, pgf.File); ok {
- cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{
- URI: puri,
- Fix: string(settings.ExtractVariable),
- Range: rng,
- })
- if err != nil {
- return nil, err
+ if cmd.Command != "" {
+ params := &protocol.ExecuteCommandParams{
+ Command: cmd.Command,
+ Arguments: cmd.Arguments,
}
- commands = append(commands, cmd)
- }
- var actions []protocol.CodeAction
- for i := range commands {
- actions = append(actions, protocol.CodeAction{
- Title: commands[i].Title,
- Kind: protocol.RefactorExtract,
- Command: &commands[i],
- })
- }
- return actions, nil
-}
-func refactorRewrite(snapshot *cache.Snapshot, pkg *cache.Package, pgf *source.ParsedGoFile, fh file.Handle, rng protocol.Range) (_ []protocol.CodeAction, rerr error) {
- // golang/go#61693: code actions were refactored to run outside of the
- // analysis framework, but as a result they lost their panic recovery.
- //
- // These code actions should never fail, but put back the panic recovery as a
- // defensive measure.
- defer func() {
- if r := recover(); r != nil {
- rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r)
+ handler := &commandHandler{
+ s: s,
+ params: params,
}
- }()
-
- var actions []protocol.CodeAction
-
- if canRemoveParameter(pkg, pgf, rng) {
- cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{
- RemoveParameter: protocol.Location{
- URI: pgf.URI,
- Range: rng,
- },
- })
+ edit, err := command.Dispatch(ctx, params, handler)
if err != nil {
- return nil, err
- }
- actions = append(actions, protocol.CodeAction{
- Title: "Refactor: remove unused parameter",
- Kind: protocol.RefactorRewrite,
- Command: &cmd,
- })
- }
-
- if action, ok := source.ConvertStringLiteral(pgf, fh, rng); ok {
- actions = append(actions, action)
- }
- start, end, err := pgf.RangePos(rng)
- if err != nil {
- return nil, err
- }
-
- var commands []protocol.Command
- if _, ok, _ := source.CanInvertIfCondition(pgf.File, start, end); ok {
- cmd, err := command.NewApplyFixCommand("Invert if condition", command.ApplyFixArgs{
- URI: pgf.URI,
- Fix: string(settings.InvertIfCondition),
- Range: rng,
- })
- if err != nil {
return nil, err
}
- commands = append(commands, cmd)
- }
-
- // N.B.: an inspector only pays for itself after ~5 passes, which means we're
- // currently not getting a good deal on this inspection.
- //
- // TODO: Consider removing the inspection after convenienceAnalyzers are removed.
- inspect := inspector.New([]*ast.File{pgf.File})
- if snapshot.Options().IsAnalyzerEnabled(fillstruct.Analyzer.Name) {
- for _, d := range fillstruct.DiagnoseFillableStructs(inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) {
- rng, err := pgf.Mapper.PosRange(pgf.Tok, d.Pos, d.End)
- if err != nil {
- return nil, err
- }
- cmd, err := command.NewApplyFixCommand(d.Message, command.ApplyFixArgs{
- URI: pgf.URI,
- Fix: string(settings.FillStruct),
- Range: rng,
- })
- if err != nil {
- return nil, err
- }
- commands = append(commands, cmd)
- }
- }
-
- for i := range commands {
- actions = append(actions, protocol.CodeAction{
- Title: commands[i].Title,
- Kind: protocol.RefactorRewrite,
- Command: &commands[i],
- })
- }
-
- if snapshot.Options().IsAnalyzerEnabled(infertypeargs.Analyzer.Name) {
- for _, d := range infertypeargs.DiagnoseInferableTypeArgs(pkg.FileSet(), inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) {
- if len(d.SuggestedFixes) != 1 {
- panic(fmt.Sprintf("unexpected number of suggested fixes from infertypeargs: %v", len(d.SuggestedFixes)))
- }
- fix := d.SuggestedFixes[0]
- var edits []protocol.TextEdit
- for _, analysisEdit := range fix.TextEdits {
- rng, err := pgf.Mapper.PosRange(pgf.Tok, analysisEdit.Pos, analysisEdit.End)
- if err != nil {
- return nil, err
- }
- edits = append(edits, protocol.TextEdit{
- Range: rng,
- NewText: string(analysisEdit.NewText),
- })
- }
- actions = append(actions, protocol.CodeAction{
- Title: "Simplify type arguments",
- Kind: protocol.RefactorRewrite,
- Edit: &protocol.WorkspaceEdit{
- DocumentChanges: documentChanges(fh, edits),
- },
- })
- }
- }
-
- return actions, nil
-}
-
-// canRemoveParameter reports whether we can remove the function parameter
-// indicated by the given [start, end) range.
-//
-// This is true if:
-// - [start, end) is contained within an unused field or parameter name
-// - ... of a non-method function declaration.
-func canRemoveParameter(pkg *cache.Package, pgf *source.ParsedGoFile, rng protocol.Range) bool {
- info, err := source.FindParam(pgf, rng)
- if err != nil {
- return false // e.g. invalid range
- }
- if info.Field == nil {
- return false // range does not span a parameter
- }
- if info.Decl.Body == nil {
- return false // external function
- }
- if len(info.Field.Names) == 0 {
- return true // no names => field is unused
- }
- if info.Name == nil {
- return false // no name is indicated
- }
- if info.Name.Name == "_" {
- return true // trivially unused
- }
-
- obj := pkg.GetTypesInfo().Defs[info.Name]
- if obj == nil {
- return false // something went wrong
- }
-
- used := false
- ast.Inspect(info.Decl.Body, func(node ast.Node) bool {
- if n, ok := node.(*ast.Ident); ok && pkg.GetTypesInfo().Uses[n] == obj {
- used = true
- }
- return !used // keep going until we find a use
- })
- return !used
-}
-
-// refactorInline returns inline actions available at the specified range.
-func refactorInline(pkg *cache.Package, pgf *source.ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) {
- start, end, err := pgf.RangePos(rng)
- if err != nil {
- return nil, err
- }
-
- // If range is within call expression, offer inline action.
- var commands []protocol.Command
- if _, fn, err := source.EnclosingStaticCall(pkg, pgf, start, end); err == nil {
- cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{
- URI: pgf.URI,
- Fix: string(settings.InlineCall),
- Range: rng,
- })
- if err != nil {
- return nil, err
+ var ok bool
+ if ca.Edit, ok = edit.(*protocol.WorkspaceEdit); !ok {
+ return nil, fmt.Errorf("unable to resolve code action %q", ca.Title)
}
- commands = append(commands, cmd)
- }
-
- // Convert commands to actions.
- var actions []protocol.CodeAction
- for i := range commands {
- actions = append(actions, protocol.CodeAction{
- Title: commands[i].Title,
- Kind: protocol.RefactorInline,
- Command: &commands[i],
- })
- }
- return actions, nil
-}
-
-func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges {
- return []protocol.DocumentChanges{
- {
- TextDocumentEdit: &protocol.TextDocumentEdit{
- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
- Version: fh.Version(),
- TextDocumentIdentifier: protocol.TextDocumentIdentifier{
- URI: fh.URI(),
- },
- },
- Edits: protocol.AsAnnotatedTextEdits(edits),
- },
- },
}
+ return ca, nil
}
// codeActionsMatchingDiagnostics fetches code actions for the provided
// diagnostics, by first attempting to unmarshal code actions directly from the
// bundled protocol.Diagnostic.Data field, and failing that by falling back on
-// fetching a matching source.Diagnostic from the set of stored diagnostics for
+// fetching a matching Diagnostic from the set of stored diagnostics for
// this file.
func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protocol.DocumentURI, snapshot *cache.Snapshot, pds []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) {
var actions []protocol.CodeAction
@@ -675,53 +219,57 @@ func codeActionsForDiagnostic(ctx context.Context, snapshot *cache.Snapshot, sd
}
changes = append(changes, documentChanges(fh, edits)...)
}
- action := protocol.CodeAction{
+ actions = append(actions, protocol.CodeAction{
Title: fix.Title,
Kind: fix.ActionKind,
Edit: &protocol.WorkspaceEdit{
DocumentChanges: changes,
},
- Command: fix.Command,
- }
- action.Diagnostics = []protocol.Diagnostic{*pd}
- actions = append(actions, action)
+ Command: fix.Command,
+ Diagnostics: []protocol.Diagnostic{*pd},
+ })
}
return actions, nil
}
-func goTest(pkg *cache.Package, pgf *source.ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) {
- fns, err := source.TestsAndBenchmarks(pkg, pgf)
- if err != nil {
- return nil, err
- }
+func (s *server) findMatchingDiagnostics(uri protocol.DocumentURI, pd protocol.Diagnostic) []*cache.Diagnostic {
+ s.diagnosticsMu.Lock()
+ defer s.diagnosticsMu.Unlock()
- var tests, benchmarks []string
- for _, fn := range fns.Tests {
- if !protocol.Intersect(fn.Rng, rng) {
- continue
- }
- tests = append(tests, fn.Name)
- }
- for _, fn := range fns.Benchmarks {
- if !protocol.Intersect(fn.Rng, rng) {
- continue
+ var sds []*cache.Diagnostic
+ for _, viewDiags := range s.diagnostics[uri].byView {
+ for _, sd := range viewDiags.diagnostics {
+ sameDiagnostic := (pd.Message == strings.TrimSpace(sd.Message) && // extra space may have been trimmed when converting to protocol.Diagnostic
+ protocol.CompareRange(pd.Range, sd.Range) == 0 &&
+ pd.Source == string(sd.Source))
+
+ if sameDiagnostic {
+ sds = append(sds, sd)
+ }
}
- benchmarks = append(benchmarks, fn.Name)
}
+ return sds
+}
- if len(tests) == 0 && len(benchmarks) == 0 {
- return nil, nil
+func (s *server) getSupportedCodeActions() []protocol.CodeActionKind {
+ allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
+ for _, kinds := range s.Options().SupportedCodeActions {
+ for kind := range kinds {
+ allCodeActionKinds[kind] = struct{}{}
+ }
}
-
- cmd, err := command.NewTestCommand("Run tests and benchmarks", pgf.URI, tests, benchmarks)
- if err != nil {
- return nil, err
+ var result []protocol.CodeActionKind
+ for kind := range allCodeActionKinds {
+ result = append(result, kind)
}
- return []protocol.CodeAction{{
- Title: cmd.Title,
- Kind: protocol.GoTest,
- Command: &cmd,
- }}, nil
+ sort.Slice(result, func(i, j int) bool {
+ return result[i] < result[j]
+ })
+ return result
}
type unit = struct{}
+
+func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges {
+ return protocol.TextEditsToDocumentChanges(fh.URI(), fh.Version(), edits)
+}
diff --git a/gopls/internal/server/code_lens.go b/gopls/internal/server/code_lens.go
index e8d7f2b4150..7e6506c2b65 100644
--- a/gopls/internal/server/code_lens.go
+++ b/gopls/internal/server/code_lens.go
@@ -10,10 +10,10 @@ import (
"sort"
"golang.org/x/tools/gopls/internal/file"
- "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/golang"
"golang.org/x/tools/gopls/internal/mod"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
@@ -28,12 +28,12 @@ func (s *server) CodeLens(ctx context.Context, params *protocol.CodeLensParams)
}
defer release()
- var lenses map[command.Command]source.LensFunc
+ var lenses map[command.Command]golang.LensFunc
switch snapshot.FileKind(fh) {
case file.Mod:
lenses = mod.LensFuncs()
case file.Go:
- lenses = source.LensFuncs()
+ lenses = golang.LensFuncs()
default:
// Unsupported file kind for a code lens.
return nil, nil
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go
index 60c71840f4f..8681db7e0ac 100644
--- a/gopls/internal/server/command.go
+++ b/gopls/internal/server/command.go
@@ -22,19 +22,18 @@ import (
"golang.org/x/mod/modfile"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/debug"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "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/golang"
+ "golang.org/x/tools/gopls/internal/progress"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/util/bug"
- "golang.org/x/tools/gopls/internal/util/maps"
"golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/gopls/internal/vulncheck/scan"
"golang.org/x/tools/internal/diff"
@@ -200,12 +199,13 @@ func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run command
return runcmd()
}
-func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs) error {
- return c.run(ctx, commandConfig{
+func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs) (*protocol.WorkspaceEdit, error) {
+ var result *protocol.WorkspaceEdit
+ err := c.run(ctx, commandConfig{
// Note: no progress here. Applying fixes should be quick.
forURI: args.URI,
}, func(ctx context.Context, deps commandDeps) error {
- edits, err := source.ApplyFix(ctx, settings.Fix(args.Fix), deps.snapshot, deps.fh, args.Range)
+ edits, err := golang.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Range)
if err != nil {
return err
}
@@ -216,10 +216,15 @@ func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs
TextDocumentEdit: &edit,
})
}
+ edit := protocol.WorkspaceEdit{
+ DocumentChanges: changes,
+ }
+ if args.ResolveEdits {
+ result = &edit
+ return nil
+ }
r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
- Edit: protocol.WorkspaceEdit{
- DocumentChanges: changes,
- },
+ Edit: edit,
})
if err != nil {
return err
@@ -229,6 +234,7 @@ func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs
}
return nil
})
+ return result, err
}
func (c *commandHandler) RegenerateCgo(ctx context.Context, args command.URIArg) error {
@@ -447,19 +453,7 @@ func (c *commandHandler) RemoveDependency(ctx context.Context, args command.Remo
}
response, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
- DocumentChanges: []protocol.DocumentChanges{
- {
- TextDocumentEdit: &protocol.TextDocumentEdit{
- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
- Version: deps.fh.Version(),
- TextDocumentIdentifier: protocol.TextDocumentIdentifier{
- URI: deps.fh.URI(),
- },
- },
- Edits: protocol.AsAnnotatedTextEdits(edits),
- },
- },
- },
+ DocumentChanges: documentChanges(deps.fh, edits),
},
})
if err != nil {
@@ -515,7 +509,7 @@ func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs
func (c *commandHandler) runTests(ctx context.Context, snapshot *cache.Snapshot, work *progress.WorkDone, uri protocol.DocumentURI, tests, benchmarks []string) error {
// TODO: fix the error reporting when this runs async.
- meta, err := source.NarrowestMetadataForFile(ctx, snapshot, uri)
+ meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, uri)
if err != nil {
return err
}
@@ -713,16 +707,9 @@ func applyFileEdits(ctx context.Context, cli protocol.Client, edits []protocol.T
if len(edits) == 0 {
return nil
}
- documentChanges := []protocol.DocumentChanges{} // must be a slice
- for _, change := range edits {
- change := change
- documentChanges = append(documentChanges, protocol.DocumentChanges{
- TextDocumentEdit: &change,
- })
- }
response, err := cli.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
- DocumentChanges: documentChanges,
+ DocumentChanges: protocol.TextDocumentEditsToDocumentChanges(edits),
},
})
if err != nil {
@@ -789,7 +776,7 @@ func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIAr
forURI: args.URI,
}, func(ctx context.Context, deps commandDeps) error {
return c.modifyState(ctx, FromToggleGCDetails, func() (*cache.Snapshot, func(), error) {
- meta, err := source.NarrowestMetadataForFile(ctx, deps.snapshot, deps.fh.URI())
+ meta, err := golang.NarrowestMetadataForFile(ctx, deps.snapshot, deps.fh.URI())
if err != nil {
return nil, nil, err
}
@@ -809,7 +796,7 @@ func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URI
progress: "Listing packages",
forURI: args.URI,
}, func(ctx context.Context, deps commandDeps) error {
- pkgs, err := source.KnownPackagePaths(ctx, deps.snapshot, deps.fh)
+ pkgs, err := golang.KnownPackagePaths(ctx, deps.snapshot, deps.fh)
for _, pkg := range pkgs {
result.Packages = append(result.Packages, string(pkg))
}
@@ -847,7 +834,7 @@ func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) (
})
}
}
- meta, err := source.NarrowestMetadataForFile(ctx, deps.snapshot, args.URI)
+ meta, err := golang.NarrowestMetadataForFile(ctx, deps.snapshot, args.URI)
if err != nil {
return err // e.g. cancelled
}
@@ -868,7 +855,7 @@ func (c *commandHandler) AddImport(ctx context.Context, args command.AddImportAr
progress: "Adding import",
forURI: args.URI,
}, func(ctx context.Context, deps commandDeps) error {
- edits, err := source.AddImport(ctx, deps.snapshot, deps.fh, args.ImportPath)
+ edits, err := golang.AddImport(ctx, deps.snapshot, deps.fh, args.ImportPath)
if err != nil {
return fmt.Errorf("could not add import: %v", err)
}
@@ -1052,7 +1039,7 @@ func (c *commandHandler) MemStats(ctx context.Context) (command.MemStatsResult,
// about the current state of the loaded workspace for the current session.
func (c *commandHandler) WorkspaceStats(ctx context.Context) (command.WorkspaceStatsResult, error) {
var res command.WorkspaceStatsResult
- res.Files.Total, res.Files.Largest, res.Files.Errs = c.s.session.Cache().FileStats()
+ res.Files = c.s.session.Cache().FileStats()
for _, view := range c.s.session.Views() {
vs, err := collectViewStats(ctx, view)
@@ -1083,7 +1070,7 @@ func collectViewStats(ctx context.Context, view *cache.View) (command.ViewStats,
}
workspacePackages := collectPackageStats(wsMD)
- var ids []source.PackageID
+ var ids []golang.PackageID
for _, mp := range wsMD {
ids = append(ids, mp.ID)
}
@@ -1267,19 +1254,25 @@ func showDocumentImpl(ctx context.Context, cli protocol.Client, url protocol.URI
}
}
-func (c *commandHandler) ChangeSignature(ctx context.Context, args command.ChangeSignatureArgs) error {
- return c.run(ctx, commandConfig{
+func (c *commandHandler) ChangeSignature(ctx context.Context, args command.ChangeSignatureArgs) (*protocol.WorkspaceEdit, error) {
+ var result *protocol.WorkspaceEdit
+ err := c.run(ctx, commandConfig{
forURI: args.RemoveParameter.URI,
}, func(ctx context.Context, deps commandDeps) error {
// For now, gopls only supports removing unused parameters.
- changes, err := source.RemoveUnusedParameter(ctx, deps.fh, args.RemoveParameter.Range, deps.snapshot)
+ changes, err := golang.RemoveUnusedParameter(ctx, deps.fh, args.RemoveParameter.Range, deps.snapshot)
if err != nil {
return err
}
+ edit := protocol.WorkspaceEdit{
+ DocumentChanges: changes,
+ }
+ if args.ResolveEdits {
+ result = &edit
+ return nil
+ }
r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
- Edit: protocol.WorkspaceEdit{
- DocumentChanges: changes,
- },
+ Edit: edit,
})
if !r.Applied {
return fmt.Errorf("failed to apply edits: %v", r.FailureReason)
@@ -1287,6 +1280,7 @@ func (c *commandHandler) ChangeSignature(ctx context.Context, args command.Chang
return nil
})
+ return result, err
}
func (c *commandHandler) DiagnoseFiles(ctx context.Context, args command.DiagnoseFilesArgs) error {
@@ -1301,47 +1295,31 @@ func (c *commandHandler) DiagnoseFiles(ctx context.Context, args command.Diagnos
ctx, done := event.Start(ctx, "lsp.server.DiagnoseFiles")
defer done()
- // TODO(adonovan): opt: parallelize the loop,
- // grouping file URIs by package and making a
- // single call to source.Analyze.
+ snapshots := make(map[*cache.Snapshot]bool)
for _, uri := range args.Files {
fh, snapshot, release, err := c.s.fileOf(ctx, uri)
if err != nil {
return err
}
- defer release()
- if snapshot.FileKind(fh) != file.Go {
+ if snapshots[snapshot] || snapshot.FileKind(fh) != file.Go {
+ release()
continue
}
- pkg, _, err := source.NarrowestPackageForFile(ctx, snapshot, uri)
- if err != nil {
- return err
- }
- pkgDiags, err := pkg.DiagnosticsForFile(ctx, uri)
- if err != nil {
- return err
- }
- adiags, err := source.Analyze(ctx, snapshot, map[source.PackageID]unit{pkg.Metadata().ID: {}}, nil /* progress tracker */)
- if err != nil {
- return err
- }
+ defer release()
+ snapshots[snapshot] = true
+ }
- // combine load/parse/type + analysis diagnostics
- var td, ad []*cache.Diagnostic
- combineDiagnostics(pkgDiags, adiags[uri], &td, &ad)
- diags := append(td, ad...)
- byURI := func(d *cache.Diagnostic) protocol.DocumentURI { return d.URI }
- c.s.updateDiagnostics(ctx, c.s.session.Views(), snapshot, maps.Group(diags, byURI), false)
- diagnostics := append(td, ad...)
-
- if err := c.s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
- URI: fh.URI(),
- Version: fh.Version(),
- Diagnostics: toProtocolDiagnostics(diagnostics),
- }); err != nil {
- return err
- }
+ var wg sync.WaitGroup
+ for snapshot := range snapshots {
+ snapshot := snapshot
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ c.s.diagnoseSnapshot(snapshot, nil, 0)
+ }()
}
+ wg.Wait()
+
return nil
})
}
diff --git a/gopls/internal/server/completion.go b/gopls/internal/server/completion.go
index 6e49d5fb346..3e019df1b14 100644
--- a/gopls/internal/server/completion.go
+++ b/gopls/internal/server/completion.go
@@ -10,9 +10,9 @@ import (
"strings"
"golang.org/x/tools/gopls/internal/file"
- "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/golang"
+ "golang.org/x/tools/gopls/internal/golang/completion"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/template"
@@ -116,7 +116,7 @@ func toProtocolCompletionItems(candidates []completion.CompletionItem, rng proto
doc := &protocol.Or_CompletionItem_documentation{
Value: protocol.MarkupContent{
Kind: protocol.Markdown,
- Value: source.CommentToMarkdown(candidate.Documentation, options),
+ Value: golang.CommentToMarkdown(candidate.Documentation, options),
},
}
if options.PreferredContentFormat != protocol.Markdown {
diff --git a/gopls/internal/server/definition.go b/gopls/internal/server/definition.go
index 74096203975..7a0eb25679b 100644
--- a/gopls/internal/server/definition.go
+++ b/gopls/internal/server/definition.go
@@ -9,8 +9,8 @@ import (
"fmt"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/template"
"golang.org/x/tools/internal/event"
@@ -36,7 +36,7 @@ func (s *server) Definition(ctx context.Context, params *protocol.DefinitionPara
case file.Tmpl:
return template.Definition(snapshot, fh, params.Position)
case file.Go:
- return source.Definition(ctx, snapshot, fh, params.Position)
+ return golang.Definition(ctx, snapshot, fh, params.Position)
default:
return nil, fmt.Errorf("can't find definitions for file type %s", kind)
}
@@ -54,7 +54,7 @@ func (s *server) TypeDefinition(ctx context.Context, params *protocol.TypeDefini
defer release()
switch kind := snapshot.FileKind(fh); kind {
case file.Go:
- return source.TypeDefinition(ctx, snapshot, fh, params.Position)
+ return golang.TypeDefinition(ctx, snapshot, fh, params.Position)
default:
return nil, fmt.Errorf("can't find type definitions for file type %s", kind)
}
diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go
index 460e119c77d..6aa01aba127 100644
--- a/gopls/internal/server/diagnostics.go
+++ b/gopls/internal/server/diagnostics.go
@@ -16,12 +16,12 @@ import (
"sync"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/mod"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/template"
"golang.org/x/tools/gopls/internal/util/maps"
@@ -263,7 +263,7 @@ func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snaps
}
// Find all packages that include this file and diagnose them in parallel.
- meta, err := source.NarrowestMetadataForFile(ctx, snapshot, uri)
+ meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, uri)
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
@@ -364,7 +364,7 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa
store("diagnosing vulnerabilities", vulnReports, vulnErr)
workspacePkgs, err := snapshot.WorkspaceMetadata(ctx)
- if s.shouldIgnoreError(ctx, snapshot, err) {
+ if s.shouldIgnoreError(snapshot, err) {
return diagnostics, ctx.Err()
}
@@ -415,9 +415,30 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa
}()
// Run type checking and go/analysis diagnosis of packages in parallel.
+ //
+ // For analysis, we use the *widest* package for each open file,
+ // for two reasons:
+ //
+ // - Correctness: some analyzers (e.g. unusedparam) depend
+ // on it. If applied to a non-test package for which a
+ // corresponding test package exists, they make assumptions
+ // that are falsified in the test package, for example that
+ // all references to unexported symbols are visible to the
+ // analysis.
+ //
+ // - Efficiency: it may yield a smaller covering set of
+ // PackageIDs for a given set of files. For example, {x.go,
+ // x_test.go} is covered by the single package x_test using
+ // "widest". (Using "narrowest", it would be covered only by
+ // the pair of packages {x, x_test}, Originally we used all
+ // covering packages, so {x.go} alone would be analyzed
+ // twice.)
var (
toDiagnose = make(map[metadata.PackageID]*metadata.Package)
- toAnalyze = make(map[metadata.PackageID]unit)
+ toAnalyze = make(map[metadata.PackageID]*metadata.Package)
+
+ // secondary index, used to eliminate narrower packages.
+ toAnalyzeWidest = make(map[golang.PackagePath]*metadata.Package)
)
for _, mp := range workspacePkgs {
var hasNonIgnored, hasOpenFile bool
@@ -432,7 +453,16 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa
if hasNonIgnored {
toDiagnose[mp.ID] = mp
if hasOpenFile {
- toAnalyze[mp.ID] = unit{}
+ if prev, ok := toAnalyzeWidest[mp.PkgPath]; ok {
+ if len(prev.CompiledGoFiles) >= len(mp.CompiledGoFiles) {
+ // Previous entry is not narrower; keep it.
+ continue
+ }
+ // Evict previous (narrower) entry.
+ delete(toAnalyze, prev.ID)
+ }
+ toAnalyze[mp.ID] = mp
+ toAnalyzeWidest[mp.PkgPath] = mp
}
}
}
@@ -466,7 +496,7 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa
var err error
// TODO(rfindley): here and above, we should avoid using the first result
// if err is non-nil (though as of today it's OK).
- analysisDiags, err = source.Analyze(ctx, snapshot, toAnalyze, s.progress)
+ analysisDiags, err = golang.Analyze(ctx, snapshot, toAnalyze, s.progress)
if err != nil {
event.Error(ctx, "warning: analyzing package", err, append(snapshot.Labels(), tag.Package.Of(keys.Join(maps.Keys(toDiagnose))))...)
return
@@ -512,7 +542,7 @@ func (s *server) gcDetailsDiagnostics(ctx context.Context, snapshot *cache.Snaps
diagnostics := make(diagMap)
for _, mp := range toGCDetail {
- gcReports, err := source.GCOptimizationDetails(ctx, snapshot, mp)
+ gcReports, err := golang.GCOptimizationDetails(ctx, snapshot, mp)
if err != nil {
event.Error(ctx, "warning: gc details", err, append(snapshot.Labels(), tag.Package.Of(string(mp.ID)))...)
continue
@@ -674,7 +704,15 @@ func (s *server) updateDiagnostics(ctx context.Context, allViews []*cache.View,
// views.
updateAndPublish := func(uri protocol.DocumentURI, f *fileDiagnostics, diags []*cache.Diagnostic) error {
current, ok := f.byView[snapshot.View()]
- if !ok || current.snapshot <= snapshot.SequenceID() {
+ // Update the stored diagnostics if:
+ // 1. we've never seen diagnostics for this view,
+ // 2. diagnostics are for an older snapshot, or
+ // 3. we're overwriting with final diagnostics
+ //
+ // In other words, we shouldn't overwrite existing diagnostics for a
+ // snapshot with non-final diagnostics. This avoids the race described at
+ // https://github.com/golang/go/issues/64765#issuecomment-1890144575.
+ if !ok || current.snapshot < snapshot.SequenceID() || (current.snapshot == snapshot.SequenceID() && final) {
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
return err
@@ -855,7 +893,7 @@ func toProtocolDiagnostics(diagnostics []*cache.Diagnostic) []protocol.Diagnosti
return reports
}
-func (s *server) shouldIgnoreError(ctx context.Context, snapshot *cache.Snapshot, err error) bool {
+func (s *server) shouldIgnoreError(snapshot *cache.Snapshot, err error) bool {
if err == nil { // if there is no error at all
return false
}
diff --git a/gopls/internal/server/folding_range.go b/gopls/internal/server/folding_range.go
index 4f471658478..cb9d0cb5d49 100644
--- a/gopls/internal/server/folding_range.go
+++ b/gopls/internal/server/folding_range.go
@@ -8,8 +8,8 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
@@ -26,14 +26,14 @@ func (s *server) FoldingRange(ctx context.Context, params *protocol.FoldingRange
if snapshot.FileKind(fh) != file.Go {
return nil, nil // empty result
}
- ranges, err := source.FoldingRange(ctx, snapshot, fh, snapshot.Options().LineFoldingOnly)
+ ranges, err := golang.FoldingRange(ctx, snapshot, fh, snapshot.Options().LineFoldingOnly)
if err != nil {
return nil, err
}
return toProtocolFoldingRanges(ranges)
}
-func toProtocolFoldingRanges(ranges []*source.FoldingRangeInfo) ([]protocol.FoldingRange, error) {
+func toProtocolFoldingRanges(ranges []*golang.FoldingRangeInfo) ([]protocol.FoldingRange, error) {
result := make([]protocol.FoldingRange, 0, len(ranges))
for _, info := range ranges {
rng := info.MappedRange.Range()
diff --git a/gopls/internal/server/format.go b/gopls/internal/server/format.go
index 19b6b62f3fc..0e6cfdce6d7 100644
--- a/gopls/internal/server/format.go
+++ b/gopls/internal/server/format.go
@@ -8,9 +8,9 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/mod"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/work"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
@@ -30,7 +30,7 @@ func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormat
case file.Mod:
return mod.Format(ctx, snapshot, fh)
case file.Go:
- return source.Format(ctx, snapshot, fh)
+ return golang.Format(ctx, snapshot, fh)
case file.Work:
return work.Format(ctx, snapshot, fh)
}
diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go
index 141fed947ae..cdaee1b973b 100644
--- a/gopls/internal/server/general.go
+++ b/gopls/internal/server/general.go
@@ -20,14 +20,15 @@ import (
"strings"
"sync"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/goversion"
+ "golang.org/x/tools/gopls/internal/util/maps"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/jsonrpc2"
)
@@ -36,7 +37,11 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ
ctx, done := event.Start(ctx, "lsp.Server.initialize")
defer done()
- telemetry.RecordClientInfo(params)
+ var clientName string
+ if params != nil && params.ClientInfo != nil {
+ clientName = params.ClientInfo.Name
+ }
+ telemetry.RecordClientInfo(clientName)
s.stateMu.Lock()
if s.state >= serverInitializing {
@@ -114,6 +119,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ
// Using CodeActionOptions is only valid if codeActionLiteralSupport is set.
codeActionProvider = &protocol.CodeActionOptions{
CodeActionKinds: s.getSupportedCodeActions(),
+ ResolveProvider: true,
}
}
var renameOpts interface{} = true
@@ -361,7 +367,7 @@ func (s *server) updateWatchedDirectories(ctx context.Context) error {
defer s.watchedGlobPatternsMu.Unlock()
// Nothing to do if the set of workspace directories is unchanged.
- if equalURISet(s.watchedGlobPatterns, patterns) {
+ if maps.SameKeys(s.watchedGlobPatterns, patterns) {
return nil
}
@@ -389,32 +395,32 @@ func watchedFilesCapabilityID(id int) string {
return fmt.Sprintf("workspace/didChangeWatchedFiles-%d", id)
}
-func equalURISet(m1, m2 map[string]struct{}) bool {
- if len(m1) != len(m2) {
- return false
- }
- for k := range m1 {
- _, ok := m2[k]
- if !ok {
- return false
- }
- }
- return true
-}
-
// registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles
// registrations to the client and updates s.watchedDirectories.
// The caller must not subsequently mutate patterns.
-func (s *server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error {
+func (s *server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[protocol.RelativePattern]unit) error {
if !s.Options().DynamicWatchedFilesSupported {
return nil
}
+
+ supportsRelativePatterns := s.Options().RelativePatternsSupported
+
s.watchedGlobPatterns = patterns
watchers := make([]protocol.FileSystemWatcher, 0, len(patterns)) // must be a slice
val := protocol.WatchChange | protocol.WatchDelete | protocol.WatchCreate
for pattern := range patterns {
+ var value any
+ if supportsRelativePatterns && pattern.BaseURI != "" {
+ value = pattern
+ } else {
+ p := pattern.Pattern
+ if pattern.BaseURI != "" {
+ p = path.Join(filepath.ToSlash(pattern.BaseURI.Path()), p)
+ }
+ value = p
+ }
watchers = append(watchers, protocol.FileSystemWatcher{
- GlobPattern: pattern,
+ GlobPattern: protocol.GlobPattern{Value: value},
Kind: &val,
})
}
@@ -467,7 +473,7 @@ func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, nam
return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err)
}
- opts := opts.Clone()
+ opts = opts.Clone()
for _, config := range configs {
if err := s.handleOptionResults(ctx, settings.SetOptions(opts, config)); err != nil {
return nil, err
@@ -487,15 +493,23 @@ func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, nam
}, nil
}
+// fetchFolderOptions makes a workspace/configuration request for the given
+// folder, and populates options with the result.
+//
+// If folder is "", fetchFolderOptions makes an unscoped request.
func (s *server) fetchFolderOptions(ctx context.Context, folder protocol.DocumentURI) (*settings.Options, error) {
opts := s.Options()
if !opts.ConfigurationSupported {
return opts, nil
}
- scope := string(folder)
+ var scopeURI *string
+ if folder != "" {
+ scope := string(folder)
+ scopeURI = &scope
+ }
configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{
Items: []protocol.ConfigurationItem{{
- ScopeURI: &scope,
+ ScopeURI: scopeURI,
Section: "gopls",
}},
},
diff --git a/gopls/internal/server/highlight.go b/gopls/internal/server/highlight.go
index 5d025644dea..45eeba77b56 100644
--- a/gopls/internal/server/highlight.go
+++ b/gopls/internal/server/highlight.go
@@ -8,8 +8,8 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/template"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
@@ -29,7 +29,7 @@ func (s *server) DocumentHighlight(ctx context.Context, params *protocol.Documen
case file.Tmpl:
return template.Highlight(ctx, snapshot, fh, params.Position)
case file.Go:
- rngs, err := source.Highlight(ctx, snapshot, fh, params.Position)
+ rngs, err := golang.Highlight(ctx, snapshot, fh, params.Position)
if err != nil {
event.Error(ctx, "no highlight", err)
}
diff --git a/gopls/internal/server/hover.go b/gopls/internal/server/hover.go
index 1a25c43f729..1ceede24ed7 100644
--- a/gopls/internal/server/hover.go
+++ b/gopls/internal/server/hover.go
@@ -8,9 +8,9 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/mod"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/template"
"golang.org/x/tools/gopls/internal/work"
@@ -37,7 +37,7 @@ func (s *server) Hover(ctx context.Context, params *protocol.HoverParams) (_ *pr
case file.Mod:
return mod.Hover(ctx, snapshot, fh, params.Position)
case file.Go:
- return source.Hover(ctx, snapshot, fh, params.Position)
+ return golang.Hover(ctx, snapshot, fh, params.Position)
case file.Tmpl:
return template.Hover(ctx, snapshot, fh, params.Position)
case file.Work:
diff --git a/gopls/internal/server/implementation.go b/gopls/internal/server/implementation.go
index 51156f98122..b462eacee8a 100644
--- a/gopls/internal/server/implementation.go
+++ b/gopls/internal/server/implementation.go
@@ -8,8 +8,8 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
@@ -32,5 +32,5 @@ func (s *server) Implementation(ctx context.Context, params *protocol.Implementa
if snapshot.FileKind(fh) != file.Go {
return nil, nil // empty result
}
- return source.Implementation(ctx, snapshot, fh, params.Position)
+ return golang.Implementation(ctx, snapshot, fh, params.Position)
}
diff --git a/gopls/internal/server/inlay_hint.go b/gopls/internal/server/inlay_hint.go
index e696df68036..88ec783e391 100644
--- a/gopls/internal/server/inlay_hint.go
+++ b/gopls/internal/server/inlay_hint.go
@@ -8,9 +8,9 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/mod"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
@@ -29,7 +29,7 @@ func (s *server) InlayHint(ctx context.Context, params *protocol.InlayHintParams
case file.Mod:
return mod.InlayHint(ctx, snapshot, fh, params.Range)
case file.Go:
- return source.InlayHint(ctx, snapshot, fh, params.Range)
+ return golang.InlayHint(ctx, snapshot, fh, params.Range)
}
return nil, nil // empty result
}
diff --git a/gopls/internal/server/link.go b/gopls/internal/server/link.go
index 511e50b5872..6ac397627d9 100644
--- a/gopls/internal/server/link.go
+++ b/gopls/internal/server/link.go
@@ -16,12 +16,12 @@ import (
"sync"
"golang.org/x/mod/modfile"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/metadata"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
@@ -121,9 +121,9 @@ func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]p
// If links are to pkg.go.dev, append module version suffixes.
// This requires the import map from the package metadata. Ignore errors.
- var depsByImpPath map[source.ImportPath]source.PackageID
+ var depsByImpPath map[golang.ImportPath]golang.PackageID
if strings.ToLower(snapshot.Options().LinkTarget) == "pkg.go.dev" {
- if meta, err := source.NarrowestMetadataForFile(ctx, snapshot, fh.URI()); err == nil {
+ if meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, fh.URI()); err == nil {
depsByImpPath = meta.DepsByImpPath
}
}
diff --git a/gopls/internal/server/prompt.go b/gopls/internal/server/prompt.go
index e5b54427a48..72c5113dc4e 100644
--- a/gopls/internal/server/prompt.go
+++ b/gopls/internal/server/prompt.go
@@ -11,7 +11,7 @@ import (
"path/filepath"
"time"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/server/references.go b/gopls/internal/server/references.go
index 1bdd85d685a..cc02d6f16b6 100644
--- a/gopls/internal/server/references.go
+++ b/gopls/internal/server/references.go
@@ -8,8 +8,8 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/gopls/internal/template"
"golang.org/x/tools/internal/event"
@@ -34,7 +34,7 @@ func (s *server) References(ctx context.Context, params *protocol.ReferenceParam
case file.Tmpl:
return template.References(ctx, snapshot, fh, params)
case file.Go:
- return source.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration)
+ return golang.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration)
}
return nil, nil // empty result
}
diff --git a/gopls/internal/server/rename.go b/gopls/internal/server/rename.go
index c4b28eb2171..946cf5092ec 100644
--- a/gopls/internal/server/rename.go
+++ b/gopls/internal/server/rename.go
@@ -10,8 +10,8 @@ import (
"path/filepath"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
@@ -30,10 +30,10 @@ func (s *server) Rename(ctx context.Context, params *protocol.RenameParams) (*pr
return nil, fmt.Errorf("cannot rename in file of type %s", kind)
}
- // Because we don't handle directory renaming within source.Rename, source.Rename returns
+ // Because we don't handle directory renaming within golang.Rename, golang.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)
+ edits, isPkgRenaming, err := golang.Rename(ctx, snapshot, fh, params.Position, params.NewName)
if err != nil {
return nil, err
}
@@ -85,7 +85,7 @@ func (s *server) PrepareRename(ctx context.Context, params *protocol.PrepareRena
// Do not return errors here, as it adds clutter.
// Returning a nil result means there is not a valid rename.
- item, usererr, err := source.PrepareRename(ctx, snapshot, fh, params.Position)
+ item, usererr, err := golang.PrepareRename(ctx, snapshot, fh, params.Position)
if err != nil {
// Return usererr here rather than err, to avoid cluttering the UI with
// internal error details.
diff --git a/gopls/internal/server/selection_range.go b/gopls/internal/server/selection_range.go
index 6090f4df17e..89142f42007 100644
--- a/gopls/internal/server/selection_range.go
+++ b/gopls/internal/server/selection_range.go
@@ -9,9 +9,9 @@ import (
"fmt"
"golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache/parsego"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/server/semantic.go b/gopls/internal/server/semantic.go
index 161c111bc3c..646f9b3d729 100644
--- a/gopls/internal/server/semantic.go
+++ b/gopls/internal/server/semantic.go
@@ -5,41 +5,17 @@
package server
import (
- "bytes"
"context"
- "errors"
"fmt"
- "go/ast"
- "go/token"
- "go/types"
- "log"
- "path/filepath"
- "sort"
- "strings"
- "time"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/cache/metadata"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/template"
- "golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
-// The LSP says that errors for the semantic token requests should only be returned
-// for exceptions (a word not otherwise defined). This code treats a too-large file
-// as an exception. On parse errors, the code does what it can.
-
-// reject full semantic token requests for large files
-const maxFullFileSize int = 100000
-
-// to control comprehensive logging of decisions (gopls semtok foo.go > /dev/null shows log output)
-// semDebug should NEVER be true in checked-in code
-const semDebug = false
-
func (s *server) SemanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) {
return s.semanticTokens(ctx, params.TextDocument, nil)
}
@@ -63,931 +39,15 @@ func (s *server) semanticTokens(ctx context.Context, td protocol.TextDocumentIde
return nil, fmt.Errorf("semantictokens are disabled")
}
- kind := snapshot.FileKind(fh)
- if kind == file.Tmpl {
- // this is a little cumbersome to avoid both exporting 'encoded' and its methods
- // and to avoid import cycles
- e := &encoded{
- ctx: ctx,
- metadataSource: snapshot,
- tokTypes: snapshot.Options().SemanticTypes,
- tokMods: snapshot.Options().SemanticMods,
- }
- add := func(line, start uint32, len uint32) {
- // TODO(adonovan): don't ignore the rng restriction, if any.
- e.add(line, start, len, tokMacro, nil)
- }
- data := func() []uint32 {
- return e.Data()
- }
- return template.SemanticTokens(ctx, snapshot, fh.URI(), add, data)
- }
- if kind != file.Go {
- return nil, nil // empty result
- }
- pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI())
- if err != nil {
- return nil, err
- }
-
- // Select range.
- var start, end token.Pos
- if rng != nil {
- var err error
- start, end, err = pgf.RangePos(*rng)
- if err != nil {
- return nil, err // e.g. invalid range
- }
- } else {
- tok := pgf.Tok
- start, end = tok.Pos(0), tok.Pos(tok.Size()) // entire file
- }
- if int(end-start) > maxFullFileSize {
- err := fmt.Errorf("semantic tokens: range %s too large (%d > %d)",
- fh.URI().Path(), end-start, maxFullFileSize)
- return nil, err
- }
-
- e := &encoded{
- ctx: ctx,
- metadataSource: snapshot,
- pgf: pgf,
- start: start,
- end: end,
- ti: pkg.GetTypesInfo(),
- pkg: pkg,
- fset: pkg.FileSet(),
- tokTypes: snapshot.Options().SemanticTypes,
- tokMods: snapshot.Options().SemanticMods,
- noStrings: snapshot.Options().NoSemanticString,
- noNumbers: snapshot.Options().NoSemanticNumber,
- }
- e.semantics()
- return &protocol.SemanticTokens{
- Data: e.Data(),
- // For delta requests, but we've never seen any.
- ResultID: fmt.Sprintf("%v", time.Now()),
- }, nil
-}
-
-func (e *encoded) semantics() {
- f := e.pgf.File
- // may not be in range, but harmless
- e.token(f.Package, len("package"), tokKeyword, nil)
- e.token(f.Name.NamePos, len(f.Name.Name), tokNamespace, nil)
- inspect := func(n ast.Node) bool {
- return e.inspector(n)
- }
- for _, d := range f.Decls {
- // only look at the decls that overlap the range
- start, end := d.Pos(), d.End()
- if end <= e.start || start >= e.end {
- continue
- }
- ast.Inspect(d, inspect)
- }
- for _, cg := range f.Comments {
- for _, c := range cg.List {
- if strings.HasPrefix(c.Text, "//go:") {
- e.godirective(c)
- continue
- }
- if !strings.Contains(c.Text, "\n") {
- e.token(c.Pos(), len(c.Text), tokComment, nil)
- continue
- }
- e.multiline(c.Pos(), c.End(), c.Text, tokComment)
- }
- }
-}
-
-type tokenType string
-
-const (
- tokNamespace tokenType = "namespace"
- tokType tokenType = "type"
- tokInterface tokenType = "interface"
- tokTypeParam tokenType = "typeParameter"
- tokParameter tokenType = "parameter"
- tokVariable tokenType = "variable"
- tokMethod tokenType = "method"
- tokFunction tokenType = "function"
- tokKeyword tokenType = "keyword"
- tokComment tokenType = "comment"
- tokString tokenType = "string"
- tokNumber tokenType = "number"
- tokOperator tokenType = "operator"
-
- tokMacro tokenType = "macro" // for templates
-)
-
-func (e *encoded) token(start token.Pos, leng int, typ tokenType, mods []string) {
- if !start.IsValid() {
- // This is not worth reporting. TODO(pjw): does it still happen?
- return
- }
- if start >= e.end || start+token.Pos(leng) <= e.start {
- return
- }
- // want a line and column from start (in LSP coordinates). Ignore line directives.
- lspRange, err := e.pgf.PosRange(start, start+token.Pos(leng))
- if err != nil {
- event.Error(e.ctx, "failed to convert to range", err)
- return
- }
- if lspRange.End.Line != lspRange.Start.Line {
- // this happens if users are typing at the end of the file, but report nothing
- return
- }
- // token is all on one line
- length := lspRange.End.Character - lspRange.Start.Character
- e.add(lspRange.Start.Line, lspRange.Start.Character, length, typ, mods)
-}
-
-func (e *encoded) add(line, start uint32, len uint32, tok tokenType, mod []string) {
- x := semItem{line, start, len, tok, mod}
- e.items = append(e.items, x)
-}
-
-// semItem represents a token found walking the parse tree
-type semItem struct {
- line, start uint32
- len uint32
- typeStr tokenType
- mods []string
-}
-
-type encoded struct {
- // the generated data
- items []semItem
-
- noStrings bool
- noNumbers bool
-
- ctx context.Context
- // metadataSource is used to resolve imports
- metadataSource metadata.Source
- tokTypes, tokMods []string
- pgf *source.ParsedGoFile
- start, end token.Pos // range of interest
- ti *types.Info
- pkg *cache.Package
- fset *token.FileSet
- // path from the root of the parse tree, used for debugging
- stack []ast.Node
-}
-
-// convert the stack to a string, for debugging
-func (e *encoded) strStack() string {
- msg := []string{"["}
- for i := len(e.stack) - 1; i >= 0; i-- {
- s := e.stack[i]
- msg = append(msg, fmt.Sprintf("%T", s)[5:])
- }
- if len(e.stack) > 0 {
- loc := e.stack[len(e.stack)-1].Pos()
- if _, err := safetoken.Offset(e.pgf.Tok, loc); err != nil {
- msg = append(msg, fmt.Sprintf("invalid position %v for %s", loc, e.pgf.URI))
- } else {
- add := safetoken.Position(e.pgf.Tok, loc)
- nm := filepath.Base(add.Filename)
- msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column))
- }
- }
- msg = append(msg, "]")
- return strings.Join(msg, " ")
-}
-
-// find the line in the source
-func (e *encoded) srcLine(x ast.Node) string {
- file := e.pgf.Tok
- line := safetoken.Line(file, x.Pos())
- start, err := safetoken.Offset(file, file.LineStart(line))
- if err != nil {
- return ""
- }
- end := start
- for ; end < len(e.pgf.Src) && e.pgf.Src[end] != '\n'; end++ {
-
- }
- ans := e.pgf.Src[start:end]
- return string(ans)
-}
-
-func (e *encoded) inspector(n ast.Node) bool {
- pop := func() {
- e.stack = e.stack[:len(e.stack)-1]
- }
- if n == nil {
- pop()
- return true
- }
- e.stack = append(e.stack, n)
- switch x := n.(type) {
- case *ast.ArrayType:
- case *ast.AssignStmt:
- e.token(x.TokPos, len(x.Tok.String()), tokOperator, nil)
- case *ast.BasicLit:
- if strings.Contains(x.Value, "\n") {
- // has to be a string.
- e.multiline(x.Pos(), x.End(), x.Value, tokString)
- break
- }
- ln := len(x.Value)
- what := tokNumber
- if x.Kind == token.STRING {
- what = tokString
- }
- e.token(x.Pos(), ln, what, nil)
- case *ast.BinaryExpr:
- e.token(x.OpPos, len(x.Op.String()), tokOperator, nil)
- case *ast.BlockStmt:
- case *ast.BranchStmt:
- e.token(x.TokPos, len(x.Tok.String()), tokKeyword, nil)
- // There's no semantic encoding for labels
- case *ast.CallExpr:
- if x.Ellipsis != token.NoPos {
- e.token(x.Ellipsis, len("..."), tokOperator, nil)
- }
- case *ast.CaseClause:
- iam := "case"
- if x.List == nil {
- iam = "default"
- }
- e.token(x.Case, len(iam), tokKeyword, nil)
- case *ast.ChanType:
- // chan | chan <- | <- chan
- switch {
- case x.Arrow == token.NoPos:
- e.token(x.Begin, len("chan"), tokKeyword, nil)
- case x.Arrow == x.Begin:
- e.token(x.Arrow, 2, tokOperator, nil)
- pos := e.findKeyword("chan", x.Begin+2, x.Value.Pos())
- e.token(pos, len("chan"), tokKeyword, nil)
- case x.Arrow != x.Begin:
- e.token(x.Begin, len("chan"), tokKeyword, nil)
- e.token(x.Arrow, 2, tokOperator, nil)
- }
- case *ast.CommClause:
- iam := len("case")
- if x.Comm == nil {
- iam = len("default")
- }
- e.token(x.Case, iam, tokKeyword, nil)
- case *ast.CompositeLit:
- case *ast.DeclStmt:
- case *ast.DeferStmt:
- e.token(x.Defer, len("defer"), tokKeyword, nil)
- case *ast.Ellipsis:
- e.token(x.Ellipsis, len("..."), tokOperator, nil)
- case *ast.EmptyStmt:
- case *ast.ExprStmt:
- case *ast.Field:
- case *ast.FieldList:
- case *ast.ForStmt:
- e.token(x.For, len("for"), tokKeyword, nil)
- case *ast.FuncDecl:
- case *ast.FuncLit:
- case *ast.FuncType:
- if x.Func != token.NoPos {
- e.token(x.Func, len("func"), tokKeyword, nil)
- }
- case *ast.GenDecl:
- e.token(x.TokPos, len(x.Tok.String()), tokKeyword, nil)
- case *ast.GoStmt:
- e.token(x.Go, len("go"), tokKeyword, nil)
- case *ast.Ident:
- e.ident(x)
- case *ast.IfStmt:
- e.token(x.If, len("if"), tokKeyword, nil)
- if x.Else != nil {
- // x.Body.End() or x.Body.End()+1, not that it matters
- pos := e.findKeyword("else", x.Body.End(), x.Else.Pos())
- e.token(pos, len("else"), tokKeyword, nil)
- }
- case *ast.ImportSpec:
- e.importSpec(x)
- pop()
- return false
- case *ast.IncDecStmt:
- e.token(x.TokPos, len(x.Tok.String()), tokOperator, nil)
- case *ast.IndexExpr:
- case *ast.IndexListExpr:
- case *ast.InterfaceType:
- e.token(x.Interface, len("interface"), tokKeyword, nil)
- case *ast.KeyValueExpr:
- case *ast.LabeledStmt:
- case *ast.MapType:
- e.token(x.Map, len("map"), tokKeyword, nil)
- case *ast.ParenExpr:
- case *ast.RangeStmt:
- e.token(x.For, len("for"), tokKeyword, nil)
- // x.TokPos == token.NoPos is legal (for range foo {})
- offset := x.TokPos
- if offset == token.NoPos {
- offset = x.For
- }
- pos := e.findKeyword("range", offset, x.X.Pos())
- e.token(pos, len("range"), tokKeyword, nil)
- case *ast.ReturnStmt:
- e.token(x.Return, len("return"), tokKeyword, nil)
- case *ast.SelectStmt:
- e.token(x.Select, len("select"), tokKeyword, nil)
- case *ast.SelectorExpr:
- case *ast.SendStmt:
- e.token(x.Arrow, len("<-"), tokOperator, nil)
- case *ast.SliceExpr:
- case *ast.StarExpr:
- e.token(x.Star, len("*"), tokOperator, nil)
- case *ast.StructType:
- e.token(x.Struct, len("struct"), tokKeyword, nil)
- case *ast.SwitchStmt:
- e.token(x.Switch, len("switch"), tokKeyword, nil)
- case *ast.TypeAssertExpr:
- if x.Type == nil {
- pos := e.findKeyword("type", x.Lparen, x.Rparen)
- e.token(pos, len("type"), tokKeyword, nil)
- }
- case *ast.TypeSpec:
- case *ast.TypeSwitchStmt:
- e.token(x.Switch, len("switch"), tokKeyword, nil)
- case *ast.UnaryExpr:
- e.token(x.OpPos, len(x.Op.String()), tokOperator, nil)
- case *ast.ValueSpec:
- // things only seen with parsing or type errors, so ignore them
- case *ast.BadDecl, *ast.BadExpr, *ast.BadStmt:
- return true
- // not going to see these
- case *ast.File, *ast.Package:
- e.unexpected(fmt.Sprintf("implement %T %s", x, safetoken.Position(e.pgf.Tok, x.Pos())))
- // other things we knowingly ignore
- case *ast.Comment, *ast.CommentGroup:
- pop()
- return false
- default:
- e.unexpected(fmt.Sprintf("failed to implement %T", x))
- }
- return true
-}
-
-func (e *encoded) ident(x *ast.Ident) {
- if e.ti == nil {
- what, mods := e.unkIdent(x)
- if what != "" {
- e.token(x.Pos(), len(x.String()), what, mods)
- }
- if semDebug {
- log.Printf(" nil %s/nil/nil %q %v %s", x.String(), what, mods, e.strStack())
- }
- return
- }
- def := e.ti.Defs[x]
- if def != nil {
- what, mods := e.definitionFor(x, def)
- if what != "" {
- e.token(x.Pos(), len(x.String()), what, mods)
- }
- if semDebug {
- log.Printf(" for %s/%T/%T got %s %v (%s)", x.String(), def, def.Type(), what, mods, e.strStack())
- }
- return
- }
- use := e.ti.Uses[x]
- tok := func(pos token.Pos, lng int, tok tokenType, mods []string) {
- e.token(pos, lng, tok, mods)
- q := "nil"
- if use != nil {
- q = fmt.Sprintf("%T", use.Type())
- }
- if semDebug {
- log.Printf(" use %s/%T/%s got %s %v (%s)", x.String(), use, q, tok, mods, e.strStack())
- }
- }
-
- switch y := use.(type) {
- case nil:
- what, mods := e.unkIdent(x)
- if what != "" {
- tok(x.Pos(), len(x.String()), what, mods)
- } else if semDebug {
- // tok() wasn't called, so didn't log
- log.Printf(" nil %s/%T/nil %q %v (%s)", x.String(), use, what, mods, e.strStack())
- }
- return
- case *types.Builtin:
- tok(x.NamePos, len(x.Name), tokFunction, []string{"defaultLibrary"})
- case *types.Const:
- mods := []string{"readonly"}
- tt := y.Type()
- if _, ok := tt.(*types.Basic); ok {
- tok(x.Pos(), len(x.String()), tokVariable, mods)
- break
- }
- if ttx, ok := tt.(*types.Named); ok {
- if x.String() == "iota" {
- e.unexpected(fmt.Sprintf("iota:%T", ttx))
- }
- if _, ok := ttx.Underlying().(*types.Basic); ok {
- tok(x.Pos(), len(x.String()), tokVariable, mods)
- break
- }
- e.unexpected(fmt.Sprintf("%q/%T", x.String(), tt))
- }
- // can this happen? Don't think so
- e.unexpected(fmt.Sprintf("%s %T %#v", x.String(), tt, tt))
- case *types.Func:
- tok(x.Pos(), len(x.Name), tokFunction, nil)
- case *types.Label:
- // nothing to map it to
- case *types.Nil:
- // nil is a predeclared identifier
- tok(x.Pos(), len("nil"), tokVariable, []string{"readonly", "defaultLibrary"})
- case *types.PkgName:
- tok(x.Pos(), len(x.Name), tokNamespace, nil)
- case *types.TypeName: // could be a tokTpeParam
- var mods []string
- if _, ok := y.Type().(*types.Basic); ok {
- mods = []string{"defaultLibrary"}
- } else if _, ok := y.Type().(*types.TypeParam); ok {
- tok(x.Pos(), len(x.String()), tokTypeParam, mods)
- break
- }
- tok(x.Pos(), len(x.String()), tokType, mods)
- case *types.Var:
- if isSignature(y) {
- tok(x.Pos(), len(x.Name), tokFunction, 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)
- }
+ switch snapshot.FileKind(fh) {
+ case file.Tmpl:
+ return template.SemanticTokens(ctx, snapshot, fh.URI())
- default:
- // can't happen
- if use == nil {
- msg := fmt.Sprintf("%#v %#v %#v", x, e.ti.Defs[x], e.ti.Uses[x])
- e.unexpected(msg)
- }
- if use.Type() != nil {
- e.unexpected(fmt.Sprintf("%s %T/%T,%#v", x.String(), use, use.Type(), use))
- } else {
- e.unexpected(fmt.Sprintf("%s %T", x.String(), use))
- }
- }
-}
-
-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 _, ok := use.(*types.Var); !ok {
- return false
- }
- v := use.Type()
- if v == nil {
- return false
- }
- if _, ok := v.(*types.Signature); ok {
- return true
- }
- return false
-}
+ case file.Go:
+ return golang.SemanticTokens(ctx, snapshot, fh, rng)
-// both e.ti.Defs and e.ti.Uses are nil. use the parse stack.
-// a lot of these only happen when the package doesn't compile
-// but in that case it is all best-effort from the parse tree
-func (e *encoded) unkIdent(x *ast.Ident) (tokenType, []string) {
- def := []string{"definition"}
- n := len(e.stack) - 2 // parent of Ident
- if n < 0 {
- e.unexpected("no stack?")
- return "", nil
- }
- switch nd := e.stack[n].(type) {
- case *ast.BinaryExpr, *ast.UnaryExpr, *ast.ParenExpr, *ast.StarExpr,
- *ast.IncDecStmt, *ast.SliceExpr, *ast.ExprStmt, *ast.IndexExpr,
- *ast.ReturnStmt, *ast.ChanType, *ast.SendStmt,
- *ast.ForStmt, // possibly incomplete
- *ast.IfStmt, /* condition */
- *ast.KeyValueExpr, // either key or value
- *ast.IndexListExpr:
- return tokVariable, nil
- case *ast.Ellipsis:
- return tokType, nil
- case *ast.CaseClause:
- if n-2 >= 0 {
- if _, ok := e.stack[n-2].(*ast.TypeSwitchStmt); ok {
- return tokType, nil
- }
- }
- return tokVariable, nil
- case *ast.ArrayType:
- if x == nd.Len {
- // or maybe a Type Param, but we can't just from the parse tree
- return tokVariable, nil
- } else {
- return tokType, nil
- }
- case *ast.MapType:
- return tokType, nil
- case *ast.CallExpr:
- if x == nd.Fun {
- return tokFunction, nil
- }
- return tokVariable, nil
- case *ast.SwitchStmt:
- return tokVariable, nil
- case *ast.TypeAssertExpr:
- if x == nd.X {
- return tokVariable, nil
- } else if x == nd.Type {
- return tokType, nil
- }
- case *ast.ValueSpec:
- for _, p := range nd.Names {
- if p == x {
- return tokVariable, def
- }
- }
- for _, p := range nd.Values {
- if p == x {
- return tokVariable, nil
- }
- }
- return tokType, nil
- case *ast.SelectorExpr: // e.ti.Selections[nd] is nil, so no help
- if n-1 >= 0 {
- if ce, ok := e.stack[n-1].(*ast.CallExpr); ok {
- // ... CallExpr SelectorExpr Ident (_.x())
- if ce.Fun == nd && nd.Sel == x {
- return tokFunction, nil
- }
- }
- }
- return tokVariable, nil
- case *ast.AssignStmt:
- for _, p := range nd.Lhs {
- // x := ..., or x = ...
- if p == x {
- if nd.Tok != token.DEFINE {
- def = nil
- }
- return tokVariable, def // '_' in _ = ...
- }
- }
- // RHS, = x
- return tokVariable, nil
- case *ast.TypeSpec: // it's a type if it is either the Name or the Type
- if x == nd.Type {
- def = nil
- }
- return tokType, def
- case *ast.Field:
- // ident could be type in a field, or a method in an interface type, or a variable
- if x == nd.Type {
- return tokType, nil
- }
- if n-2 >= 0 {
- _, okit := e.stack[n-2].(*ast.InterfaceType)
- _, okfl := e.stack[n-1].(*ast.FieldList)
- if okit && okfl {
- return tokMethod, def
- }
- }
- return tokVariable, nil
- case *ast.LabeledStmt, *ast.BranchStmt:
- // nothing to report
- case *ast.CompositeLit:
- if nd.Type == x {
- return tokType, nil
- }
- return tokVariable, nil
- case *ast.RangeStmt:
- if nd.Tok != token.DEFINE {
- def = nil
- }
- return tokVariable, def
- case *ast.FuncDecl:
- return tokFunction, def
default:
- msg := fmt.Sprintf("%T undexpected: %s %s%q", nd, x.Name, e.strStack(), e.srcLine(x))
- e.unexpected(msg)
- }
- return "", nil
-}
-
-func isDeprecated(n *ast.CommentGroup) bool {
- if n == nil {
- return false
- }
- for _, c := range n.List {
- if strings.HasPrefix(c.Text, "// Deprecated") {
- return true
- }
- }
- return false
-}
-
-func (e *encoded) definitionFor(x *ast.Ident, def types.Object) (tokenType, []string) {
- // PJW: def == types.Label? probably a nothing
- // PJW: look into replacing these syntactic tests with types more generally
- mods := []string{"definition"}
- for i := len(e.stack) - 1; i >= 0; i-- {
- s := e.stack[i]
- switch y := s.(type) {
- case *ast.AssignStmt, *ast.RangeStmt:
- if x.Name == "_" {
- return "", nil // not really a variable
- }
- return tokVariable, mods
- case *ast.GenDecl:
- if isDeprecated(y.Doc) {
- mods = append(mods, "deprecated")
- }
- if y.Tok == token.CONST {
- mods = append(mods, "readonly")
- }
- return tokVariable, mods
- case *ast.FuncDecl:
- // If x is immediately under a FuncDecl, it is a function or method
- if i == len(e.stack)-2 {
- if isDeprecated(y.Doc) {
- mods = append(mods, "deprecated")
- }
- if y.Recv != nil {
- return tokMethod, mods
- }
- return tokFunction, mods
- }
- // if x < ... < FieldList < FuncDecl, this is the receiver, a variable
- // PJW: maybe not. it might be a typeparameter in the type of the receiver
- if _, ok := e.stack[i+1].(*ast.FieldList); ok {
- if _, ok := def.(*types.TypeName); ok {
- return tokTypeParam, mods
- }
- return tokVariable, nil
- }
- // if x < ... < FieldList < FuncType < FuncDecl, this is a param
- return tokParameter, mods
- case *ast.FuncType: // is it in the TypeParams?
- if isTypeParam(x, y) {
- return tokTypeParam, mods
- }
- return tokParameter, mods
- case *ast.InterfaceType:
- return tokMethod, mods
- case *ast.TypeSpec:
- // GenDecl/Typespec/FuncType/FieldList/Field/Ident
- // (type A func(b uint64)) (err error)
- // b and err should not be tokType, but tokVaraible
- // and in GenDecl/TpeSpec/StructType/FieldList/Field/Ident
- // (type A struct{b uint64}
- // but on type B struct{C}), C is a type, but is not being defined.
- // GenDecl/TypeSpec/FieldList/Field/Ident is a typeParam
- if _, ok := e.stack[i+1].(*ast.FieldList); ok {
- return tokTypeParam, mods
- }
- fldm := e.stack[len(e.stack)-2]
- if fld, ok := fldm.(*ast.Field); ok {
- // if len(fld.names) == 0 this is a tokType, being used
- if len(fld.Names) == 0 {
- return tokType, nil
- }
- return tokVariable, mods
- }
- return tokType, mods
- }
- }
- // can't happen
- msg := fmt.Sprintf("failed to find the decl for %s", safetoken.Position(e.pgf.Tok, x.Pos()))
- e.unexpected(msg)
- return "", []string{""}
-}
-
-func isTypeParam(x *ast.Ident, y *ast.FuncType) bool {
- tp := y.TypeParams
- if tp == nil {
- return false
- }
- for _, p := range tp.List {
- for _, n := range p.Names {
- if x == n {
- return true
- }
- }
- }
- return false
-}
-
-func (e *encoded) multiline(start, end token.Pos, val string, tok tokenType) {
- f := e.fset.File(start)
- // the hard part is finding the lengths of lines. include the \n
- leng := func(line int) int {
- n := f.LineStart(line)
- if line >= f.LineCount() {
- return f.Size() - int(n)
- }
- return int(f.LineStart(line+1) - n)
- }
- spos := safetoken.StartPosition(e.fset, start)
- epos := safetoken.EndPosition(e.fset, end)
- sline := spos.Line
- eline := epos.Line
- // first line is from spos.Column to end
- e.token(start, leng(sline)-spos.Column, tok, nil) // leng(sline)-1 - (spos.Column-1)
- for i := sline + 1; i < eline; i++ {
- // intermediate lines are from 1 to end
- e.token(f.LineStart(i), leng(i)-1, tok, nil) // avoid the newline
- }
- // last line is from 1 to epos.Column
- e.token(f.LineStart(eline), epos.Column-1, tok, nil) // columns are 1-based
-}
-
-// findKeyword finds a keyword rather than guessing its location
-func (e *encoded) findKeyword(keyword string, start, end token.Pos) token.Pos {
- offset := int(start) - e.pgf.Tok.Base()
- last := int(end) - e.pgf.Tok.Base()
- buf := e.pgf.Src
- idx := bytes.Index(buf[offset:last], []byte(keyword))
- if idx != -1 {
- return start + token.Pos(idx)
- }
- //(in unparsable programs: type _ <-<-chan int)
- e.unexpected(fmt.Sprintf("not found:%s %v", keyword, safetoken.StartPosition(e.fset, start)))
- return token.NoPos
-}
-
-func (e *encoded) Data() []uint32 {
- // binary operators, at least, will be out of order
- sort.Slice(e.items, func(i, j int) bool {
- if e.items[i].line != e.items[j].line {
- return e.items[i].line < e.items[j].line
- }
- return e.items[i].start < e.items[j].start
- })
- typeMap, modMap := e.maps()
- // each semantic token needs five values
- // (see Integer Encoding for Tokens in the LSP spec)
- x := make([]uint32, 5*len(e.items))
- var j int
- var last semItem
- for i := 0; i < len(e.items); i++ {
- 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] = item.line - last.line
- }
- x[j+1] = item.start
- if j > 0 && x[j] == 0 {
- x[j+1] = item.start - last.start
- }
- x[j+2] = item.len
- x[j+3] = uint32(typ)
- mask := 0
- 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 = item
- }
- return x[:j]
-}
-
-func (e *encoded) importSpec(d *ast.ImportSpec) {
- // a local package name or the last component of the Path
- if d.Name != nil {
- nm := d.Name.String()
- if nm != "_" && nm != "." {
- e.token(d.Name.Pos(), len(nm), tokNamespace, nil)
- }
- return // don't mark anything for . or _
- }
- importPath := metadata.UnquoteImportPath(d)
- if importPath == "" {
- return
- }
- // Import strings are implementation defined. Try to match with parse information.
- depID := e.pkg.Metadata().DepsByImpPath[importPath]
- if depID == "" {
- return
- }
- depMD := e.metadataSource.Metadata(depID)
- if depMD == nil {
- // unexpected, but impact is that maybe some import is not colored
- return
- }
- // Check whether the original literal contains the package's declared name.
- j := strings.LastIndex(d.Path.Value, string(depMD.Name))
- if j == -1 {
- // Package name does not match import path, so there is nothing to report.
- return
- }
- // Report virtual declaration at the position of the substring.
- start := d.Path.Pos() + token.Pos(j)
- e.token(start, len(depMD.Name), tokNamespace, nil)
-}
-
-// log unexpected state
-func (e *encoded) unexpected(msg string) {
- if semDebug {
- panic(msg)
- }
- event.Error(e.ctx, e.strStack(), errors.New(msg))
-}
-
-func (e *encoded) maps() (map[tokenType]int, map[string]int) {
- tmap := make(map[tokenType]int)
- mmap := make(map[string]int)
- for i, t := range e.tokTypes {
- tmap[tokenType(t)] = i
- }
- for i, m := range e.tokMods {
- mmap[m] = 1 << uint(i) // go 1.12 compatibility
- }
- return tmap, mmap
-}
-
-var godirectives = map[string]struct{}{
- // https://pkg.go.dev/cmd/compile
- "noescape": {},
- "uintptrescapes": {},
- "noinline": {},
- "norace": {},
- "nosplit": {},
- "linkname": {},
-
- // https://pkg.go.dev/go/build
- "build": {},
- "binary-only-package": {},
- "embed": {},
-}
-
-// Tokenize godirective at the start of the comment c, if any, and the surrounding comment.
-// If there is any failure, emits the entire comment as a tokComment token.
-// Directives are highlighted as-is, even if used incorrectly. Typically there are
-// dedicated analyzers that will warn about misuse.
-func (e *encoded) godirective(c *ast.Comment) {
- // First check if '//go:directive args...' is a valid directive.
- directive, args, _ := strings.Cut(c.Text, " ")
- kind, _ := stringsCutPrefix(directive, "//go:")
- if _, ok := godirectives[kind]; !ok {
- // Unknown go: directive.
- e.token(c.Pos(), len(c.Text), tokComment, nil)
- return
- }
-
- // Make the 'go:directive' part stand out, the rest is comments.
- e.token(c.Pos(), len("//"), tokComment, nil)
-
- directiveStart := c.Pos() + token.Pos(len("//"))
- e.token(directiveStart, len(directive[len("//"):]), tokNamespace, nil)
-
- if len(args) > 0 {
- tailStart := c.Pos() + token.Pos(len(directive)+len(" "))
- e.token(tailStart, len(args), tokComment, nil)
- }
-}
-
-// Go 1.20 strings.CutPrefix.
-func stringsCutPrefix(s, prefix string) (after string, found bool) {
- if !strings.HasPrefix(s, prefix) {
- return s, false
+ // TODO(adonovan): should return an error!
+ return nil, nil // empty result
}
- return s[len(prefix):], true
}
diff --git a/gopls/internal/server/server.go b/gopls/internal/server/server.go
index 3b807697eeb..ff0c2682917 100644
--- a/gopls/internal/server/server.go
+++ b/gopls/internal/server/server.go
@@ -12,9 +12,9 @@ import (
"os"
"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/cache"
+ "golang.org/x/tools/gopls/internal/progress"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/event"
)
@@ -89,7 +89,7 @@ type server struct {
// that the server should watch changes.
// The map field may be reassigned but the map is immutable.
watchedGlobPatternsMu sync.Mutex
- watchedGlobPatterns map[string]unit
+ watchedGlobPatterns map[protocol.RelativePattern]unit
watchRegistrationCount int
diagnosticsMu sync.Mutex
diff --git a/gopls/internal/server/signature_help.go b/gopls/internal/server/signature_help.go
index fb2262afe9c..712c35b820c 100644
--- a/gopls/internal/server/signature_help.go
+++ b/gopls/internal/server/signature_help.go
@@ -8,8 +8,8 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
)
@@ -28,7 +28,7 @@ func (s *server) SignatureHelp(ctx context.Context, params *protocol.SignatureHe
return nil, nil // empty result
}
- info, activeParameter, err := source.SignatureHelp(ctx, snapshot, fh, params.Position)
+ info, activeParameter, err := golang.SignatureHelp(ctx, snapshot, fh, params.Position)
if err != nil {
event.Error(ctx, "no signature help", err, tag.Position.Of(params.Position))
return nil, nil // sic? There could be many reasons for failure.
diff --git a/gopls/internal/server/symbols.go b/gopls/internal/server/symbols.go
index 6eb0057f29e..3442318b352 100644
--- a/gopls/internal/server/symbols.go
+++ b/gopls/internal/server/symbols.go
@@ -8,8 +8,8 @@ import (
"context"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/template"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
@@ -30,7 +30,7 @@ func (s *server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSy
case file.Tmpl:
docSymbols, err = template.DocumentSymbols(snapshot, fh)
case file.Go:
- docSymbols, err = source.DocumentSymbols(ctx, snapshot, fh)
+ docSymbols, err = golang.DocumentSymbols(ctx, snapshot, fh)
default:
return nil, nil // empty result
}
diff --git a/gopls/internal/server/text_synchronization.go b/gopls/internal/server/text_synchronization.go
index 30385d0335f..ae353940b00 100644
--- a/gopls/internal/server/text_synchronization.go
+++ b/gopls/internal/server/text_synchronization.go
@@ -12,10 +12,10 @@ import (
"path/filepath"
"sync"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "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/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/event/tag"
"golang.org/x/tools/internal/jsonrpc2"
@@ -162,7 +162,7 @@ func (s *server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri proto
if err != nil {
return err
}
- isGenerated := source.IsGenerated(ctx, snapshot, uri)
+ isGenerated := golang.IsGenerated(ctx, snapshot, uri)
release()
if isGenerated {
diff --git a/gopls/internal/server/unimplemented.go b/gopls/internal/server/unimplemented.go
index 6f897adf564..c293ee167a7 100644
--- a/gopls/internal/server/unimplemented.go
+++ b/gopls/internal/server/unimplemented.go
@@ -10,7 +10,7 @@ import (
"context"
"fmt"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/jsonrpc2"
)
@@ -102,10 +102,6 @@ func (s *server) Resolve(context.Context, *protocol.InlayHint) (*protocol.InlayH
return nil, notImplemented("Resolve")
}
-func (s *server) ResolveCodeAction(context.Context, *protocol.CodeAction) (*protocol.CodeAction, error) {
- return nil, notImplemented("ResolveCodeAction")
-}
-
func (s *server) ResolveCodeLens(context.Context, *protocol.CodeLens) (*protocol.CodeLens, error) {
return nil, notImplemented("ResolveCodeLens")
}
diff --git a/gopls/internal/server/workspace.go b/gopls/internal/server/workspace.go
index aa9eed1d496..1a3c0864d33 100644
--- a/gopls/internal/server/workspace.go
+++ b/gopls/internal/server/workspace.go
@@ -9,8 +9,8 @@ import (
"fmt"
"sync"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/server/workspace_symbol.go b/gopls/internal/server/workspace_symbol.go
index 3b96a38e742..9eafeb015ad 100644
--- a/gopls/internal/server/workspace_symbol.go
+++ b/gopls/internal/server/workspace_symbol.go
@@ -7,9 +7,9 @@ package server
import (
"context"
- "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/cache"
+ "golang.org/x/tools/gopls/internal/golang"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/telemetry"
"golang.org/x/tools/internal/event"
)
@@ -37,5 +37,5 @@ func (s *server) Symbol(ctx context.Context, params *protocol.WorkspaceSymbolPar
defer release()
snapshots = append(snapshots, snapshot)
}
- return source.WorkspaceSymbols(ctx, matcher, style, snapshots, params.Query)
+ return golang.WorkspaceSymbols(ctx, matcher, style, snapshots, params.Query)
}
diff --git a/gopls/internal/settings/analyzer.go b/gopls/internal/settings/analyzer.go
index f9376930564..d855aa21a0d 100644
--- a/gopls/internal/settings/analyzer.go
+++ b/gopls/internal/settings/analyzer.go
@@ -6,23 +6,7 @@ package settings
import (
"golang.org/x/tools/go/analysis"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
-)
-
-// A Fix identifies kinds of suggested fix, both in Analyzer.Fix and in the
-// ApplyFix subcommand (see ExecuteCommand and ApplyFixArgs.Fix).
-type Fix string
-
-const (
- FillStruct Fix = "fill_struct"
- StubMethods Fix = "stub_methods"
- UndeclaredName Fix = "undeclared_name"
- ExtractVariable Fix = "extract_variable"
- ExtractFunction Fix = "extract_function"
- ExtractMethod Fix = "extract_method"
- InlineCall Fix = "inline_call"
- InvertIfCondition Fix = "invert_if_condition"
- AddEmbedImport Fix = "add_embed_import"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// Analyzer augments a go/analysis analyzer with additional LSP configuration.
@@ -36,18 +20,15 @@ type Analyzer struct {
// Most clients should use the IsEnabled method.
Enabled bool
- // Fix is the name of the suggested fix name used to invoke the suggested
- // fixes for the analyzer. It is non-empty if we expect this analyzer to
- // provide its fix separately from its diagnostics. That is, we should apply
- // the analyzer's suggested fixes through a Command, not a TextEdit.
- Fix Fix
-
- // ActionKind is the set of kinds of code action this analyzer produces.
+ // ActionKinds is the set of kinds of code action this analyzer produces.
// If empty, the set is just QuickFix.
- ActionKind []protocol.CodeActionKind
+ ActionKinds []protocol.CodeActionKind
// Severity is the severity set for diagnostics reported by this
// analyzer. If left unset it defaults to Warning.
+ //
+ // Note: diagnostics with severity protocol.SeverityHint do not show up in
+ // the VS Code "problems" tab.
Severity protocol.DiagnosticSeverity
// Tag is extra tags (unnecessary, deprecated, etc) for diagnostics
diff --git a/gopls/internal/settings/api_json.go b/gopls/internal/settings/api_json.go
index 7f6760947d4..9324992cc3a 100644
--- a/gopls/internal/settings/api_json.go
+++ b/gopls/internal/settings/api_json.go
@@ -214,7 +214,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 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",
+ 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 \"unusedvariable\": true // Enable the unusedvariable analyzer.\n}\n...\n```\n",
EnumKeys: EnumKeys{
ValueType: "bool",
Keys: []EnumKey{
@@ -303,6 +303,11 @@ var GeneratedAPIJSON = &APIJSON{
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",
},
+ {
+ Name: "\"fillreturns\"",
+ Doc: "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.",
+ Default: "true",
+ },
{
Name: "\"httpresponse\"",
Doc: "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.",
@@ -313,6 +318,11 @@ var GeneratedAPIJSON = &APIJSON{
Doc: "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.",
Default: "true",
},
+ {
+ Name: "\"infertypeargs\"",
+ Doc: "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n",
+ Default: "true",
+ },
{
Name: "\"loopclosure\"",
Doc: "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nNote: An iteration variable can only outlive a loop iteration in Go versions <=1.21.\nIn Go 1.22 and later, the loop variable lifetimes changed to create a new\niteration variable per loop iteration. (See go.dev/issue/60078.)\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v [\"\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\t<> :=\n\nor a new function declaration, such as:\n\n\tfunc <>(inferred parameters) {\n\t\tpanic(\"implement me!\")\n\t}",
+ 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.",
@@ -420,14 +450,19 @@ var GeneratedAPIJSON = &APIJSON{
},
{
Name: "\"unusedparams\"",
- Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo reduce false positives it ignores:\n- methods\n- parameters that do not have a name or have the name '_' (the blank identifier)\n- functions in test files\n- functions with empty bodies or those with just a return stmt",
- Default: "false",
+ Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.",
+ Default: "true",
},
{
Name: "\"unusedresult\"",
Doc: "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side\neffects, so it is always a mistake to discard the result. Other\nfunctions may return an error that must not be ignored, or a cleanup\noperation that must be called. This analyzer reports calls to\nfunctions like these when the result of the call is ignored.\n\nThe set of functions may be controlled using flags.",
Default: "true",
},
+ {
+ Name: "\"unusedvariable\"",
+ Doc: "check for unused variables and suggest fixes",
+ Default: "false",
+ },
{
Name: "\"unusedwrite\"",
Doc: "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}",
@@ -438,46 +473,6 @@ var GeneratedAPIJSON = &APIJSON{
Doc: "check for constraints that could be simplified to \"any\"",
Default: "false",
},
- {
- Name: "\"fillreturns\"",
- Doc: "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.",
- Default: "true",
- },
- {
- Name: "\"nonewvars\"",
- Doc: "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\n\tz := 1\n\tz := 2\n\nwill turn into\n\n\tz := 1\n\tz = 2",
- Default: "true",
- },
- {
- Name: "\"noresultvalues\"",
- Doc: "suggested fixes for unexpected return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\" or \"too many return values\".\nFor example:\n\n\tfunc z() { return nil }\n\nwill turn into\n\n\tfunc z() { return }",
- Default: "true",
- },
- {
- Name: "\"undeclaredname\"",
- 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\t<> :=\n\nor a new function declaration, such as:\n\n\tfunc <>(inferred parameters) {\n\t\tpanic(\"implement me!\")\n\t}",
- Default: "true",
- },
- {
- Name: "\"unusedvariable\"",
- Doc: "check for unused variables and suggest fixes",
- 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",
- Default: "true",
- },
- {
- Name: "\"infertypeargs\"",
- Doc: "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n",
- Default: "true",
- },
- {
- Name: "\"stubmethods\"",
- Doc: "detect missing methods and fix with stub implementations\n\nThis analyzer detects type-checking errors due to missing methods\nin assignments from concrete types to interface types, and offers\na suggested fix that will create a set of stub methods so that\nthe concrete type satisfies the interface.\n\nFor example, this function will not compile because the value\nNegativeErr{} does not implement the \"error\" interface:\n\n\tfunc sqrt(x float64) (float64, error) {\n\t\tif x < 0 {\n\t\t\treturn 0, NegativeErr{} // error: missing method\n\t\t}\n\t\t...\n\t}\n\n\ttype NegativeErr struct{}\n\nThis analyzer will suggest a fix to declare this method:\n\n\t// Error implements error.Error.\n\tfunc (NegativeErr) Error() string {\n\t\tpanic(\"unimplemented\")\n\t}\n\n(At least, it appears to behave that way, but technically it\ndoesn't use the SuggestedFix mechanism and the stub is created by\nlogic in gopls's source.stub function.)",
- Default: "true",
- },
},
},
Default: "{}",
@@ -739,16 +734,18 @@ var GeneratedAPIJSON = &APIJSON{
ArgDoc: "{\n\t// Names and Values must have the same length.\n\t\"Names\": []string,\n\t\"Values\": []int64,\n}",
},
{
- Command: "gopls.apply_fix",
- Title: "Apply a fix",
- Doc: "Applies a fix to a region of source code.",
- ArgDoc: "{\n\t// The fix to apply.\n\t\"Fix\": string,\n\t// The file URI for the document to fix.\n\t\"URI\": string,\n\t// The document range to scan for fixes.\n\t\"Range\": {\n\t\t\"start\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t},\n}",
+ Command: "gopls.apply_fix",
+ Title: "Apply a fix",
+ Doc: "Applies a fix to a region of source code.",
+ ArgDoc: "{\n\t// The name of the fix to apply.\n\t//\n\t// For fixes suggested by analyzers, this is a string constant\n\t// advertised by the analyzer that matches the Category of\n\t// the analysis.Diagnostic with a SuggestedFix containing no edits.\n\t//\n\t// For fixes suggested by code actions, this is a string agreed\n\t// upon by the code action and golang.ApplyFix.\n\t\"Fix\": string,\n\t// The file URI for the document to fix.\n\t\"URI\": string,\n\t// The document range to scan for fixes.\n\t\"Range\": {\n\t\t\"start\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t},\n\t// Whether to resolve and return the edits.\n\t\"ResolveEdits\": bool,\n}",
+ ResultDoc: "{\n\t// Holds changes to existing resources.\n\t\"changes\": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit,\n\t// Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes\n\t// are either an array of `TextDocumentEdit`s to express changes to n different text documents\n\t// where each text document edit addresses a specific version of a text document. Or it can contain\n\t// above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations.\n\t//\n\t// Whether a client supports versioned document edits is expressed via\n\t// `workspace.workspaceEdit.documentChanges` client capability.\n\t//\n\t// If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then\n\t// only plain `TextEdit`s using the `changes` property are supported.\n\t\"documentChanges\": []{\n\t\t\"TextDocumentEdit\": {\n\t\t\t\"textDocument\": { ... },\n\t\t\t\"edits\": { ... },\n\t\t},\n\t\t\"RenameFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"oldUri\": string,\n\t\t\t\"newUri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t},\n\t// A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and\n\t// delete file / folder operations.\n\t//\n\t// Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`.\n\t//\n\t// @since 3.16.0\n\t\"changeAnnotations\": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation,\n}",
},
{
- Command: "gopls.change_signature",
- Title: "Perform a \"change signature\" refactoring",
- Doc: "This command is experimental, currently only supporting parameter removal.\nIts signature will certainly change in the future (pun intended).",
- ArgDoc: "{\n\t\"RemoveParameter\": {\n\t\t\"uri\": string,\n\t\t\"range\": {\n\t\t\t\"start\": { ... },\n\t\t\t\"end\": { ... },\n\t\t},\n\t},\n}",
+ Command: "gopls.change_signature",
+ Title: "Perform a \"change signature\" refactoring",
+ Doc: "This command is experimental, currently only supporting parameter removal.\nIts signature will certainly change in the future (pun intended).",
+ ArgDoc: "{\n\t\"RemoveParameter\": {\n\t\t\"uri\": string,\n\t\t\"range\": {\n\t\t\t\"start\": { ... },\n\t\t\t\"end\": { ... },\n\t\t},\n\t},\n\t// Whether to resolve and return the edits.\n\t\"ResolveEdits\": bool,\n}",
+ ResultDoc: "{\n\t// Holds changes to existing resources.\n\t\"changes\": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit,\n\t// Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes\n\t// are either an array of `TextDocumentEdit`s to express changes to n different text documents\n\t// where each text document edit addresses a specific version of a text document. Or it can contain\n\t// above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations.\n\t//\n\t// Whether a client supports versioned document edits is expressed via\n\t// `workspace.workspaceEdit.documentChanges` client capability.\n\t//\n\t// If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then\n\t// only plain `TextEdit`s using the `changes` property are supported.\n\t\"documentChanges\": []{\n\t\t\"TextDocumentEdit\": {\n\t\t\t\"textDocument\": { ... },\n\t\t\t\"edits\": { ... },\n\t\t},\n\t\t\"RenameFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"oldUri\": string,\n\t\t\t\"newUri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t},\n\t// A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and\n\t// delete file / folder operations.\n\t//\n\t// Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`.\n\t//\n\t// @since 3.16.0\n\t\"changeAnnotations\": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation,\n}",
},
{
Command: "gopls.check_upgrades",
@@ -773,7 +770,7 @@ var GeneratedAPIJSON = &APIJSON{
Title: "Get known vulncheck result",
Doc: "Fetch the result of latest vulnerability check (`govulncheck`).",
ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}",
- ResultDoc: "map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result",
+ ResultDoc: "map[golang.org/x/tools/gopls/internal/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result",
},
{
Command: "gopls.gc_details",
@@ -1069,6 +1066,12 @@ var GeneratedAPIJSON = &APIJSON{
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",
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment",
},
+ {
+ Name: "fillreturns",
+ Doc: "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.",
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns",
+ Default: true,
+ },
{
Name: "httpresponse",
Doc: "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.",
@@ -1081,6 +1084,12 @@ var GeneratedAPIJSON = &APIJSON{
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ifaceassert",
Default: true,
},
+ {
+ Name: "infertypeargs",
+ Doc: "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n",
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs",
+ Default: true,
+ },
{
Name: "loopclosure",
Doc: "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nNote: An iteration variable can only outlive a loop iteration in Go versions <=1.21.\nIn Go 1.22 and later, the loop variable lifetimes changed to create a new\niteration variable per loop iteration. (See go.dev/issue/60078.)\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v [\"\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\t<> :=\n\nor a new function declaration, such as:\n\n\tfunc <>(inferred parameters) {\n\t\tpanic(\"implement me!\")\n\t}",
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname",
+ 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.",
@@ -1207,9 +1240,10 @@ var GeneratedAPIJSON = &APIJSON{
Default: true,
},
{
- Name: "unusedparams",
- Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo reduce false positives it ignores:\n- methods\n- parameters that do not have a name or have the name '_' (the blank identifier)\n- functions in test files\n- functions with empty bodies or those with just a return stmt",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams",
+ Name: "unusedparams",
+ Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.",
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams",
+ Default: true,
},
{
Name: "unusedresult",
@@ -1217,6 +1251,11 @@ var GeneratedAPIJSON = &APIJSON{
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedresult",
Default: true,
},
+ {
+ Name: "unusedvariable",
+ Doc: "check for unused variables and suggest fixes",
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable",
+ },
{
Name: "unusedwrite",
Doc: "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}",
@@ -1227,53 +1266,6 @@ var GeneratedAPIJSON = &APIJSON{
Doc: "check for constraints that could be simplified to \"any\"",
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany",
},
- {
- Name: "fillreturns",
- Doc: "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns",
- Default: true,
- },
- {
- Name: "nonewvars",
- Doc: "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\n\tz := 1\n\tz := 2\n\nwill turn into\n\n\tz := 1\n\tz = 2",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/nonewvars",
- Default: true,
- },
- {
- Name: "noresultvalues",
- Doc: "suggested fixes for unexpected return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\" or \"too many return values\".\nFor example:\n\n\tfunc z() { return nil }\n\nwill turn into\n\n\tfunc z() { return }",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/noresultvars",
- Default: true,
- },
- {
- Name: "undeclaredname",
- 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\t<> :=\n\nor a new function declaration, such as:\n\n\tfunc <>(inferred parameters) {\n\t\tpanic(\"implement me!\")\n\t}",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname",
- Default: true,
- },
- {
- Name: "unusedvariable",
- Doc: "check for unused variables and suggest fixes",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable",
- },
- {
- 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",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillstruct",
- Default: true,
- },
- {
- Name: "infertypeargs",
- Doc: "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs",
- Default: true,
- },
- {
- Name: "stubmethods",
- Doc: "detect missing methods and fix with stub implementations\n\nThis analyzer detects type-checking errors due to missing methods\nin assignments from concrete types to interface types, and offers\na suggested fix that will create a set of stub methods so that\nthe concrete type satisfies the interface.\n\nFor example, this function will not compile because the value\nNegativeErr{} does not implement the \"error\" interface:\n\n\tfunc sqrt(x float64) (float64, error) {\n\t\tif x < 0 {\n\t\t\treturn 0, NegativeErr{} // error: missing method\n\t\t}\n\t\t...\n\t}\n\n\ttype NegativeErr struct{}\n\nThis analyzer will suggest a fix to declare this method:\n\n\t// Error implements error.Error.\n\tfunc (NegativeErr) Error() string {\n\t\tpanic(\"unimplemented\")\n\t}\n\n(At least, it appears to behave that way, but technically it\ndoesn't use the SuggestedFix mechanism and the stub is created by\nlogic in gopls's source.stub function.)",
- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods",
- Default: true,
- },
},
Hints: []*HintJSON{
{
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go
index 0f8b4f37791..752effab3ef 100644
--- a/gopls/internal/settings/default.go
+++ b/gopls/internal/settings/default.go
@@ -9,8 +9,8 @@ import (
"time"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
)
var (
@@ -115,12 +115,12 @@ func DefaultOptions(overrides ...func(*Options)) *Options {
ReportAnalysisProgressAfter: 5 * time.Second,
TelemetryPrompt: false,
LinkifyShowMessage: false,
+ IncludeReplaceInWorkspace: true,
+ ZeroConfig: true,
},
Hooks: Hooks{
URLRegexp: urlRegexp(),
- DefaultAnalyzers: defaultAnalyzers(),
- TypeErrorAnalyzers: typeErrorAnalyzers(),
- ConvenienceAnalyzers: convenienceAnalyzers(),
+ DefaultAnalyzers: analyzers(),
StaticcheckAnalyzers: map[string]*Analyzer{},
},
}
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go
index a5afc6fecf1..2f080596846 100644
--- a/gopls/internal/settings/settings.go
+++ b/gopls/internal/settings/settings.go
@@ -54,7 +54,6 @@ import (
"golang.org/x/tools/gopls/internal/analysis/deprecated"
"golang.org/x/tools/gopls/internal/analysis/embeddirective"
"golang.org/x/tools/gopls/internal/analysis/fillreturns"
- "golang.org/x/tools/gopls/internal/analysis/fillstruct"
"golang.org/x/tools/gopls/internal/analysis/infertypeargs"
"golang.org/x/tools/gopls/internal/analysis/nonewvars"
"golang.org/x/tools/gopls/internal/analysis/noresultvalues"
@@ -67,8 +66,8 @@ import (
"golang.org/x/tools/gopls/internal/analysis/unusedvariable"
"golang.org/x/tools/gopls/internal/analysis/useany"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
)
type Annotation string
@@ -103,7 +102,7 @@ type Options struct {
// TODO(rfindley): refactor to simplify this function. We no longer need the
// different categories of analyzer.
func (opts *Options) IsAnalyzerEnabled(name string) bool {
- for _, amap := range []map[string]*Analyzer{opts.DefaultAnalyzers, opts.TypeErrorAnalyzers, opts.ConvenienceAnalyzers, opts.StaticcheckAnalyzers} {
+ for _, amap := range []map[string]*Analyzer{opts.DefaultAnalyzers, opts.StaticcheckAnalyzers} {
for _, analyzer := range amap {
if analyzer.Analyzer.Name == name && analyzer.IsEnabled(opts) {
return true
@@ -122,6 +121,7 @@ type ClientOptions struct {
DynamicConfigurationSupported bool
DynamicRegistrationSemanticTokensSupported bool
DynamicWatchedFilesSupported bool
+ RelativePatternsSupported bool
PreferredContentFormat protocol.MarkupKind
LineFoldingOnly bool
HierarchicalDocumentSymbolSupport bool
@@ -131,6 +131,7 @@ type ClientOptions struct {
CompletionTags bool
CompletionDeprecated bool
SupportedResourceOperations []protocol.ResourceOperationKind
+ CodeActionResolveOptions []string
}
// ServerOptions holds LSP-specific configuration that is provided by the
@@ -326,7 +327,7 @@ type DiagnosticOptions struct {
// ...
// "analyses": {
// "unreachable": false, // Disable the unreachable analyzer.
- // "unusedparams": true // Enable the unusedparams analyzer.
+ // "unusedvariable": true // Enable the unusedvariable analyzer.
// }
// ...
// ```
@@ -456,8 +457,6 @@ type Hooks struct {
GofumptFormat func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error)
DefaultAnalyzers map[string]*Analyzer
- TypeErrorAnalyzers map[string]*Analyzer
- ConvenienceAnalyzers map[string]*Analyzer
StaticcheckAnalyzers map[string]*Analyzer
}
@@ -554,6 +553,17 @@ type InternalOptions struct {
// LinkifyShowMessage controls whether the client wants gopls
// to linkify links in showMessage. e.g. [go.dev](https://go.dev).
LinkifyShowMessage bool
+
+ // IncludeReplaceInWorkspace controls whether locally replaced modules in a
+ // go.mod file are treated like workspace modules.
+ // Or in other words, if a go.mod file with local replaces behaves like a
+ // go.work file.
+ IncludeReplaceInWorkspace bool
+
+ // ZeroConfig enables the zero-config algorithm for workspace layout,
+ // dynamically creating build configurations for different modules,
+ // directories, and GOOS/GOARCH combinations to cover open files.
+ ZeroConfig bool
}
type SubdirWatchPatterns string
@@ -720,6 +730,7 @@ func (o *Options) ForClientCapabilities(clientName *protocol.ClientInfo, caps pr
o.DynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration
o.DynamicRegistrationSemanticTokensSupported = caps.TextDocument.SemanticTokens.DynamicRegistration
o.DynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration
+ o.RelativePatternsSupported = caps.Workspace.DidChangeWatchedFiles.RelativePatternSupport
// Check which types of content format are supported by this client.
if hover := caps.TextDocument.Hover; hover != nil && len(hover.ContentFormat) > 0 {
@@ -748,6 +759,11 @@ func (o *Options) ForClientCapabilities(clientName *protocol.ClientInfo, caps pr
} else if caps.TextDocument.Completion.CompletionItem.DeprecatedSupport {
o.CompletionDeprecated = true
}
+
+ // Check if the client supports code actions resolving.
+ if caps.TextDocument.CodeAction.DataSupport && caps.TextDocument.CodeAction.ResolveSupport != nil {
+ o.CodeActionResolveOptions = caps.TextDocument.CodeAction.ResolveSupport.Properties
+ }
}
func (o *Options) Clone() *Options {
@@ -794,8 +810,6 @@ func (o *Options) Clone() *Options {
return dst
}
result.DefaultAnalyzers = copyAnalyzerMap(o.DefaultAnalyzers)
- result.TypeErrorAnalyzers = copyAnalyzerMap(o.TypeErrorAnalyzers)
- result.ConvenienceAnalyzers = copyAnalyzerMap(o.ConvenienceAnalyzers)
result.StaticcheckAnalyzers = copyAnalyzerMap(o.StaticcheckAnalyzers)
return result
}
@@ -822,9 +836,6 @@ func (o *Options) enableAllExperimentMaps() {
if _, ok := o.Codelenses[string(command.RunGovulncheck)]; !ok {
o.Codelenses[string(command.RunGovulncheck)] = true
}
- 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
}
@@ -1113,6 +1124,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{})
result.deprecated("")
case "allowModfileModifications":
+ result.softErrorf("gopls setting \"allowModfileModifications\" is deprecated.\nPlease comment on https://go.dev/issue/65546 if this impacts your workflow.")
result.setBool(&o.AllowModfileModifications)
case "allowImplicitNetworkAccess":
@@ -1145,9 +1157,16 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{})
case "telemetryPrompt":
result.setBool(&o.TelemetryPrompt)
+
case "linkifyShowMessage":
result.setBool(&o.LinkifyShowMessage)
+ case "includeReplaceInWorkspace":
+ result.setBool(&o.IncludeReplaceInWorkspace)
+
+ case "zeroConfig":
+ result.setBool(&o.ZeroConfig)
+
// Replaced settings.
case "experimentalDisabledAnalyses":
result.deprecated("analyses")
@@ -1377,71 +1396,25 @@ func (r *OptionResult) setStringSlice(s *[]string) {
}
}
-func typeErrorAnalyzers() map[string]*Analyzer {
+func analyzers() map[string]*Analyzer {
return map[string]*Analyzer{
- fillreturns.Analyzer.Name: {
- Analyzer: fillreturns.Analyzer,
- // TODO(rfindley): is SourceFixAll even necessary here? Is that not implied?
- ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
- Enabled: true,
- },
- nonewvars.Analyzer.Name: {
- Analyzer: nonewvars.Analyzer,
- Enabled: true,
- },
- noresultvalues.Analyzer.Name: {
- Analyzer: noresultvalues.Analyzer,
- Enabled: true,
- },
- undeclaredname.Analyzer.Name: {
- Analyzer: undeclaredname.Analyzer,
- Fix: UndeclaredName,
- Enabled: true,
- },
- unusedvariable.Analyzer.Name: {
- Analyzer: unusedvariable.Analyzer,
- Enabled: false,
- },
- }
-}
-
-// TODO(golang/go#61559): remove convenience analyzers now that they are not
-// used from the analysis framework.
-func convenienceAnalyzers() map[string]*Analyzer {
- return map[string]*Analyzer{
- fillstruct.Analyzer.Name: {
- Analyzer: fillstruct.Analyzer,
- Fix: FillStruct,
- Enabled: true,
- ActionKind: []protocol.CodeActionKind{protocol.RefactorRewrite},
- },
- stubmethods.Analyzer.Name: {
- Analyzer: stubmethods.Analyzer,
- Fix: StubMethods,
+ // The traditional vet suite:
+ appends.Analyzer.Name: {Analyzer: appends.Analyzer, Enabled: true},
+ asmdecl.Analyzer.Name: {Analyzer: asmdecl.Analyzer, Enabled: true},
+ assign.Analyzer.Name: {Analyzer: assign.Analyzer, Enabled: true},
+ atomic.Analyzer.Name: {Analyzer: atomic.Analyzer, Enabled: true},
+ bools.Analyzer.Name: {Analyzer: bools.Analyzer, Enabled: true},
+ buildtag.Analyzer.Name: {Analyzer: buildtag.Analyzer, Enabled: true},
+ cgocall.Analyzer.Name: {Analyzer: cgocall.Analyzer, Enabled: true},
+ composite.Analyzer.Name: {Analyzer: composite.Analyzer, Enabled: true},
+ copylock.Analyzer.Name: {Analyzer: copylock.Analyzer, Enabled: true},
+ defers.Analyzer.Name: {Analyzer: defers.Analyzer, Enabled: true},
+ deprecated.Analyzer.Name: {
+ Analyzer: deprecated.Analyzer,
Enabled: true,
+ Severity: protocol.SeverityHint,
+ Tag: []protocol.DiagnosticTag{protocol.Deprecated},
},
- infertypeargs.Analyzer.Name: {
- Analyzer: infertypeargs.Analyzer,
- Enabled: true,
- ActionKind: []protocol.CodeActionKind{protocol.RefactorRewrite},
- },
- }
-}
-
-func defaultAnalyzers() map[string]*Analyzer {
- return map[string]*Analyzer{
- // The traditional vet suite:
- appends.Analyzer.Name: {Analyzer: appends.Analyzer, Enabled: true},
- asmdecl.Analyzer.Name: {Analyzer: asmdecl.Analyzer, Enabled: true},
- assign.Analyzer.Name: {Analyzer: assign.Analyzer, Enabled: true},
- atomic.Analyzer.Name: {Analyzer: atomic.Analyzer, Enabled: true},
- bools.Analyzer.Name: {Analyzer: bools.Analyzer, Enabled: true},
- buildtag.Analyzer.Name: {Analyzer: buildtag.Analyzer, Enabled: true},
- cgocall.Analyzer.Name: {Analyzer: cgocall.Analyzer, Enabled: true},
- composite.Analyzer.Name: {Analyzer: composite.Analyzer, Enabled: true},
- copylock.Analyzer.Name: {Analyzer: copylock.Analyzer, Enabled: true},
- defers.Analyzer.Name: {Analyzer: defers.Analyzer, Enabled: true},
- deprecated.Analyzer.Name: {Analyzer: deprecated.Analyzer, Enabled: true, Severity: protocol.SeverityHint, Tag: []protocol.DiagnosticTag{protocol.Deprecated}},
directive.Analyzer.Name: {Analyzer: directive.Analyzer, Enabled: true},
errorsas.Analyzer.Name: {Analyzer: errorsas.Analyzer, Enabled: true},
httpresponse.Analyzer.Name: {Analyzer: httpresponse.Analyzer, Enabled: true},
@@ -1469,32 +1442,48 @@ func defaultAnalyzers() map[string]*Analyzer {
shadow.Analyzer.Name: {Analyzer: shadow.Analyzer, Enabled: false},
sortslice.Analyzer.Name: {Analyzer: sortslice.Analyzer, Enabled: true},
testinggoroutine.Analyzer.Name: {Analyzer: testinggoroutine.Analyzer, Enabled: true},
- unusedparams.Analyzer.Name: {Analyzer: unusedparams.Analyzer, Enabled: false},
+ unusedparams.Analyzer.Name: {Analyzer: unusedparams.Analyzer, Enabled: true},
unusedwrite.Analyzer.Name: {Analyzer: unusedwrite.Analyzer, Enabled: false},
useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false},
- timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true},
- embeddirective.Analyzer.Name: {
- Analyzer: embeddirective.Analyzer,
+ infertypeargs.Analyzer.Name: {
+ Analyzer: infertypeargs.Analyzer,
Enabled: true,
- Fix: AddEmbedImport,
+ Severity: protocol.SeverityHint,
},
+ timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true},
+ embeddirective.Analyzer.Name: {Analyzer: embeddirective.Analyzer, Enabled: true},
// gofmt -s suite:
simplifycompositelit.Analyzer.Name: {
- Analyzer: simplifycompositelit.Analyzer,
- Enabled: true,
- ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
+ Analyzer: simplifycompositelit.Analyzer,
+ Enabled: true,
+ ActionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
},
simplifyrange.Analyzer.Name: {
- Analyzer: simplifyrange.Analyzer,
- Enabled: true,
- ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
+ Analyzer: simplifyrange.Analyzer,
+ Enabled: true,
+ ActionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
},
simplifyslice.Analyzer.Name: {
- Analyzer: simplifyslice.Analyzer,
- Enabled: true,
- ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
+ Analyzer: simplifyslice.Analyzer,
+ Enabled: true,
+ ActionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix},
},
+
+ // Type error analyzers.
+ // These analyzers enrich go/types errors with suggested fixes.
+ fillreturns.Analyzer.Name: {Analyzer: fillreturns.Analyzer, Enabled: true},
+ nonewvars.Analyzer.Name: {Analyzer: nonewvars.Analyzer, Enabled: true},
+ noresultvalues.Analyzer.Name: {Analyzer: noresultvalues.Analyzer, Enabled: true},
+ stubmethods.Analyzer.Name: {Analyzer: stubmethods.Analyzer, Enabled: true},
+ undeclaredname.Analyzer.Name: {Analyzer: undeclaredname.Analyzer, Enabled: true},
+ // TODO(rfindley): why isn't the 'unusedvariable' analyzer enabled, if it
+ // is only enhancing type errors with suggested fixes?
+ //
+ // In particular, enabling this analyzer could cause unused variables to be
+ // greyed out, (due to the 'deletions only' fix). That seems like a nice UI
+ // feature.
+ unusedvariable.Analyzer.Name: {Analyzer: unusedvariable.Analyzer, Enabled: false},
}
}
diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go
index 9234afe2166..7123b3d477d 100644
--- a/gopls/internal/telemetry/cmd/stacks/stacks.go
+++ b/gopls/internal/telemetry/cmd/stacks/stacks.go
@@ -17,6 +17,7 @@ import (
"log"
"net/http"
"net/url"
+ "sort"
"strings"
"time"
@@ -72,15 +73,30 @@ func main() {
if prog.Program == "golang.org/x/tools/gopls" && len(prog.Stacks) > 0 {
total++
+ // Include applicable client names (e.g. vscode, eglot).
+ var clients []string
+ var clientSuffix string
+ for key := range prog.Counters {
+ client := strings.TrimPrefix(key, "gopls/client:")
+ if client != key {
+ clients = append(clients, client)
+ }
+ }
+ sort.Strings(clients)
+ if len(clients) > 0 {
+ clientSuffix = " " + strings.Join(clients, ",")
+ }
+
// Ignore @devel versions as they correspond to
// ephemeral (and often numerous) variations of
// the program as we work on a fix to a bug.
if prog.Version == "devel" {
continue
}
- info := fmt.Sprintf("%s@%s %s %s/%s",
+ info := fmt.Sprintf("%s@%s %s %s/%s%s",
prog.Program, prog.Version,
- prog.GoVersion, prog.GOOS, prog.GOARCH)
+ prog.GoVersion, prog.GOOS, prog.GOARCH,
+ clientSuffix)
for stack, count := range prog.Stacks {
counts := stacks[stack]
if counts == nil {
diff --git a/gopls/internal/telemetry/telemetry.go b/gopls/internal/telemetry/telemetry.go
index 2ce284c2bfd..deab5d22eb1 100644
--- a/gopls/internal/telemetry/telemetry.go
+++ b/gopls/internal/telemetry/telemetry.go
@@ -12,10 +12,30 @@ import (
"golang.org/x/telemetry"
"golang.org/x/telemetry/counter"
+ "golang.org/x/telemetry/crashmonitor"
"golang.org/x/telemetry/upload"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
)
+// CounterOpen calls [counter.Open].
+func CounterOpen() {
+ counter.Open()
+}
+
+// StartCrashMonitor calls [crashmonitor.Start].
+func StartCrashMonitor() {
+ crashmonitor.Start()
+}
+
+// CrashMonitorSupported calls [crashmonitor.Supported].
+func CrashMonitorSupported() bool {
+ return crashmonitor.Supported()
+}
+
+// NewStackCounter calls [counter.NewStack].
+func NewStackCounter(name string, depth int) *counter.StackCounter {
+ return counter.NewStack(name, depth)
+}
+
// Mode calls x/telemetry.Mode.
func Mode() string {
return telemetry.Mode()
@@ -32,41 +52,42 @@ func Upload() {
}
// RecordClientInfo records gopls client info.
-func RecordClientInfo(params *protocol.ParamInitialize) {
- client := "gopls/client:other"
- if params != nil && params.ClientInfo != nil {
- switch params.ClientInfo.Name {
- case "Visual Studio Code":
- client = "gopls/client:vscode"
- case "Visual Studio Code - Insiders":
- client = "gopls/client:vscode-insiders"
- case "VSCodium":
- client = "gopls/client:vscodium"
- case "code-server":
- // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19
- client = "gopls/client:code-server"
- case "Eglot":
- // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html
- client = "gopls/client:eglot"
- case "govim":
- // https://github.com/govim/govim/pull/1189
- client = "gopls/client:govim"
- case "Neovim":
- // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361
- client = "gopls/client:neovim"
- case "coc.nvim":
- // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994
- client = "gopls/client:coc.nvim"
- case "Sublime Text LSP":
- // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493
- client = "gopls/client:sublimetext"
- default:
- // at least accumulate the client name locally
- counter.New(fmt.Sprintf("gopls/client-other:%s", params.ClientInfo.Name)).Inc()
- // but also record client:other
+func RecordClientInfo(clientName string) {
+ key := "gopls/client:other"
+ switch clientName {
+ case "Visual Studio Code":
+ key = "gopls/client:vscode"
+ case "Visual Studio Code - Insiders":
+ key = "gopls/client:vscode-insiders"
+ case "VSCodium":
+ key = "gopls/client:vscodium"
+ case "code-server":
+ // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19
+ key = "gopls/client:code-server"
+ case "Eglot":
+ // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html
+ key = "gopls/client:eglot"
+ case "govim":
+ // https://github.com/govim/govim/pull/1189
+ key = "gopls/client:govim"
+ case "Neovim":
+ // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361
+ key = "gopls/client:neovim"
+ case "coc.nvim":
+ // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994
+ key = "gopls/client:coc.nvim"
+ case "Sublime Text LSP":
+ // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493
+ key = "gopls/client:sublimetext"
+ default:
+ // Accumulate at least a local counter for an unknown
+ // client name, but also fall through to count it as
+ // ":other" for collection.
+ if clientName != "" {
+ counter.New(fmt.Sprintf("gopls/client-other:%s", clientName)).Inc()
}
}
- counter.Inc(client)
+ counter.Inc(key)
}
// RecordViewGoVersion records the Go minor version number (1.x) used for a view.
diff --git a/gopls/internal/telemetry/telemetry_go118.go b/gopls/internal/telemetry/telemetry_go118.go
index 7d0f11c7443..12b7803f1a7 100644
--- a/gopls/internal/telemetry/telemetry_go118.go
+++ b/gopls/internal/telemetry/telemetry_go118.go
@@ -7,7 +7,22 @@
package telemetry
-import "golang.org/x/tools/gopls/internal/lsp/protocol"
+// This file defines dummy implementations of telemetry operations to
+// permit building with go1.18. Until we drop support for go1.18,
+// gopls may not refer to the telemetry module directly, but must go
+// through this file.
+
+func CounterOpen() {}
+
+func StartCrashMonitor() {}
+
+func CrashMonitorSupported() bool { return false }
+
+func NewStackCounter(string, int) dummyCounter { return dummyCounter{} }
+
+type dummyCounter struct{}
+
+func (dummyCounter) Inc() {}
func Mode() string {
return "local"
@@ -20,8 +35,7 @@ func SetMode(mode string) error {
func Upload() {
}
-func RecordClientInfo(params *protocol.ParamInitialize) {
-}
+func RecordClientInfo(string) {}
func RecordViewGoVersion(x int) {
}
diff --git a/gopls/internal/telemetry/telemetry_test.go b/gopls/internal/telemetry/telemetry_test.go
index 9cb56120c4e..b52ac7093c7 100644
--- a/gopls/internal/telemetry/telemetry_test.go
+++ b/gopls/internal/telemetry/telemetry_test.go
@@ -19,8 +19,8 @@ import (
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/counter/countertest" // requires go1.21+
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/telemetry"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/util/bug"
diff --git a/gopls/internal/template/completion.go b/gopls/internal/template/completion.go
index 06ad2e52ae8..dfacefc938e 100644
--- a/gopls/internal/template/completion.go
+++ b/gopls/internal/template/completion.go
@@ -12,9 +12,9 @@ import (
"go/token"
"strings"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// information needed for completion
diff --git a/gopls/internal/template/completion_test.go b/gopls/internal/template/completion_test.go
index 0fc478842ee..8e1bdbf0535 100644
--- a/gopls/internal/template/completion_test.go
+++ b/gopls/internal/template/completion_test.go
@@ -10,7 +10,7 @@ import (
"strings"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
func init() {
diff --git a/gopls/internal/template/highlight.go b/gopls/internal/template/highlight.go
index ba61f0c7a56..39812cfd0ba 100644
--- a/gopls/internal/template/highlight.go
+++ b/gopls/internal/template/highlight.go
@@ -9,9 +9,9 @@ import (
"fmt"
"regexp"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, loc protocol.Position) ([]protocol.DocumentHighlight, error) {
diff --git a/gopls/internal/template/implementations.go b/gopls/internal/template/implementations.go
index d82f027deb5..19a27620b57 100644
--- a/gopls/internal/template/implementations.go
+++ b/gopls/internal/template/implementations.go
@@ -11,9 +11,10 @@ import (
"strconv"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/semtok"
)
// line number (1-based) and message
@@ -155,7 +156,7 @@ func References(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
return ans, nil
}
-func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, spn protocol.DocumentURI, add func(line, start, len uint32), d func() []uint32) (*protocol.SemanticTokens, error) {
+func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, spn protocol.DocumentURI) (*protocol.SemanticTokens, error) {
fh, err := snapshot.ReadFile(ctx, spn)
if err != nil {
return nil, err
@@ -166,6 +167,20 @@ func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, spn protocol.
}
p := parseBuffer(buf)
+ var items []semtok.Token
+ add := func(line, start, len uint32) {
+ if len == 0 {
+ return // vscode doesn't like 0-length Tokens
+ }
+ // TODO(adonovan): don't ignore the rng restriction, if any.
+ items = append(items, semtok.Token{
+ Line: line,
+ Start: start,
+ Len: len,
+ Type: semtok.TokMacro,
+ })
+ }
+
for _, t := range p.Tokens() {
if t.Multiline {
la, ca := p.LineCol(t.Start)
@@ -184,9 +199,15 @@ func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, spn protocol.
line, col := p.LineCol(t.Start)
add(line, col, uint32(sz))
}
- data := d()
+ const noStrings = false
+ const noNumbers = false
ans := &protocol.SemanticTokens{
- Data: data,
+ Data: semtok.Encode(
+ items,
+ noStrings,
+ noNumbers,
+ snapshot.Options().SemanticTypes,
+ snapshot.Options().SemanticMods),
// for small cache, some day. for now, the LSP client ignores this
// (that is, when the LSP client starts returning these, we can cache)
ResultID: fmt.Sprintf("%v", time.Now()),
diff --git a/gopls/internal/template/parse.go b/gopls/internal/template/parse.go
index f9ef18f965b..448a5ab51e8 100644
--- a/gopls/internal/template/parse.go
+++ b/gopls/internal/template/parse.go
@@ -22,7 +22,7 @@ import (
"unicode/utf8"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/template/symbols.go b/gopls/internal/template/symbols.go
index e0fcab8a3e6..fcbaec43c54 100644
--- a/gopls/internal/template/symbols.go
+++ b/gopls/internal/template/symbols.go
@@ -11,9 +11,9 @@ import (
"text/template/parse"
"unicode/utf8"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/test/integration/bench/bench_test.go b/gopls/internal/test/integration/bench/bench_test.go
index 67ef02533f5..5ae46c9ae3d 100644
--- a/gopls/internal/test/integration/bench/bench_test.go
+++ b/gopls/internal/test/integration/bench/bench_test.go
@@ -22,7 +22,7 @@ import (
"golang.org/x/tools/gopls/internal/cmd"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/gopls/internal/util/bug"
@@ -57,7 +57,7 @@ const runAsGopls = "_GOPLS_BENCH_RUN_AS_GOPLS"
func TestMain(m *testing.M) {
bug.PanicOnBugs = true
if os.Getenv(runAsGopls) == "true" {
- tool.Main(context.Background(), cmd.New("gopls", "", nil, hooks.Options), os.Args[1:])
+ tool.Main(context.Background(), cmd.New(hooks.Options), os.Args[1:])
os.Exit(0)
}
event.SetExporter(nil) // don't log to stderr
diff --git a/gopls/internal/test/integration/bench/codeaction_test.go b/gopls/internal/test/integration/bench/codeaction_test.go
index 8f1660b3d52..fe89500da82 100644
--- a/gopls/internal/test/integration/bench/codeaction_test.go
+++ b/gopls/internal/test/integration/bench/codeaction_test.go
@@ -9,7 +9,7 @@ import (
"sync/atomic"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
func BenchmarkCodeAction(b *testing.B) {
diff --git a/gopls/internal/test/integration/bench/completion_test.go b/gopls/internal/test/integration/bench/completion_test.go
index 9492a9ce2d0..bbbba0e3fd1 100644
--- a/gopls/internal/test/integration/bench/completion_test.go
+++ b/gopls/internal/test/integration/bench/completion_test.go
@@ -10,12 +10,12 @@ import (
"sync/atomic"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
)
-// TODO(rfindley): update these completion tests to run on multiple repos.
+var completionGOPATH = flag.String("completion_gopath", "", "if set, use this GOPATH for BenchmarkCompletion")
type completionBenchOptions struct {
file, locationRegexp string
@@ -25,6 +25,7 @@ type completionBenchOptions struct {
beforeCompletion func(*Env) // run before each completion
}
+// Deprecated: new tests should be expressed in BenchmarkCompletion.
func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
repo := getRepo(b, "tools")
_ = repo.sharedEnv(b) // ensure cache is warm
@@ -146,7 +147,7 @@ func (c *completer) _() {
}, b)
}
-type completionFollowingEditTest struct {
+type completionTest struct {
repo string
name string
file string // repo-relative file to create
@@ -154,7 +155,7 @@ type completionFollowingEditTest struct {
locationRegexp string // regexp for completion
}
-var completionFollowingEditTests = []completionFollowingEditTest{
+var completionTests = []completionTest{
{
"tools",
"selector",
@@ -168,6 +169,32 @@ func (c *completer) _() {
`,
`func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
},
+ {
+ "tools",
+ "unimportedident",
+ "internal/lsp/source/completion/completion2.go",
+ `
+package completion
+
+func (c *completer) _() {
+ lo
+}
+`,
+ `lo()`,
+ },
+ {
+ "tools",
+ "unimportedselector",
+ "internal/lsp/source/completion/completion2.go",
+ `
+package completion
+
+func (c *completer) _() {
+ log.
+}
+`,
+ `log\.()`,
+ },
{
"kubernetes",
"selector",
@@ -213,14 +240,18 @@ func (p *Pivot) _() {
//
// Edits force type-checked packages to be invalidated, so we want to measure
// how long it takes before completion results are available.
-func BenchmarkCompletionFollowingEdit(b *testing.B) {
- for _, test := range completionFollowingEditTests {
+func BenchmarkCompletion(b *testing.B) {
+ for _, test := range completionTests {
b.Run(fmt.Sprintf("%s_%s", test.repo, test.name), func(b *testing.B) {
- for _, completeUnimported := range []bool{true, false} {
- b.Run(fmt.Sprintf("completeUnimported=%v", completeUnimported), func(b *testing.B) {
- for _, budget := range []string{"0s", "100ms"} {
- b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) {
- runCompletionFollowingEdit(b, test, completeUnimported, budget)
+ for _, followingEdit := range []bool{true, false} {
+ b.Run(fmt.Sprintf("edit=%v", followingEdit), func(b *testing.B) {
+ for _, completeUnimported := range []bool{true, false} {
+ b.Run(fmt.Sprintf("unimported=%v", completeUnimported), func(b *testing.B) {
+ for _, budget := range []string{"0s", "100ms"} {
+ b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) {
+ runCompletion(b, test, followingEdit, completeUnimported, budget)
+ })
+ }
})
}
})
@@ -229,13 +260,20 @@ func BenchmarkCompletionFollowingEdit(b *testing.B) {
}
}
+// For optimizing unimported completion, it can be useful to benchmark with a
+// huge GOMODCACHE.
var gomodcache = flag.String("gomodcache", "", "optional GOMODCACHE for unimported completion benchmarks")
-func runCompletionFollowingEdit(b *testing.B, test completionFollowingEditTest, completeUnimported bool, budget string) {
+func runCompletion(b *testing.B, test completionTest, followingEdit, completeUnimported bool, budget string) {
repo := getRepo(b, test.repo)
- sharedEnv := repo.sharedEnv(b) // ensure cache is warm
+ gopath := *completionGOPATH
+ if gopath == "" {
+ // use a warm GOPATH
+ sharedEnv := repo.sharedEnv(b)
+ gopath = sharedEnv.Sandbox.GOPATH()
+ }
envvars := map[string]string{
- "GOPATH": sharedEnv.Sandbox.GOPATH(), // use the warm cache
+ "GOPATH": gopath,
}
if *gomodcache != "" {
@@ -248,7 +286,7 @@ func runCompletionFollowingEdit(b *testing.B, test completionFollowingEditTest,
"completeUnimported": completeUnimported,
"completionBudget": budget,
},
- }, "completionFollowingEdit", false)
+ }, "completion", false)
defer env.Close()
env.CreateBuffer(test.file, "// __TEST_PLACEHOLDER_0__\n"+test.content)
@@ -278,12 +316,14 @@ func runCompletionFollowingEdit(b *testing.B, test completionFollowingEditTest,
b.ResetTimer()
- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "completionFollowingEdit")); stopAndRecord != nil {
+ if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "completion")); stopAndRecord != nil {
defer stopAndRecord()
}
for i := 0; i < b.N; i++ {
- editPlaceholder()
+ if followingEdit {
+ editPlaceholder()
+ }
loc := env.RegexpSearch(test.file, test.locationRegexp)
env.Completion(loc)
}
diff --git a/gopls/internal/test/integration/bench/didchange_test.go b/gopls/internal/test/integration/bench/didchange_test.go
index f006a5a97ba..22e7ca2a11b 100644
--- a/gopls/internal/test/integration/bench/didchange_test.go
+++ b/gopls/internal/test/integration/bench/didchange_test.go
@@ -10,7 +10,7 @@ import (
"testing"
"time"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration/fake"
)
diff --git a/gopls/internal/test/integration/bench/iwl_test.go b/gopls/internal/test/integration/bench/iwl_test.go
index 2fa9724dc0e..07a5d9070d0 100644
--- a/gopls/internal/test/integration/bench/iwl_test.go
+++ b/gopls/internal/test/integration/bench/iwl_test.go
@@ -7,8 +7,8 @@ package bench
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
)
@@ -16,24 +16,20 @@ import (
// BenchmarkInitialWorkspaceLoad benchmarks the initial workspace load time for
// a new editing session.
func BenchmarkInitialWorkspaceLoad(b *testing.B) {
- tests := []struct {
- repo string
- file string
- }{
- {"google-cloud-go", "httpreplay/httpreplay.go"},
- {"istio", "pkg/fuzz/util.go"},
- {"kubernetes", "pkg/controller/lookup_cache.go"},
- {"kuma", "api/generic/insights.go"},
- {"oracle", "dataintegration/data_type.go"},
- {"pkgsite", "internal/frontend/server.go"},
- {"starlark", "starlark/eval.go"},
- {"tools", "internal/lsp/cache/snapshot.go"},
- {"hashiform", "internal/provider/provider.go"},
+ repoNames := []string{
+ "google-cloud-go",
+ "istio",
+ "kubernetes",
+ "kuma",
+ "oracle",
+ "pkgsite",
+ "starlark",
+ "tools",
+ "hashiform",
}
-
- for _, test := range tests {
- b.Run(test.repo, func(b *testing.B) {
- repo := getRepo(b, test.repo)
+ for _, repoName := range repoNames {
+ b.Run(repoName, func(b *testing.B) {
+ repo := getRepo(b, repoName)
// get the (initialized) shared env to ensure the cache is warm.
// Reuse its GOPATH so that we get cache hits for things in the module
// cache.
@@ -41,13 +37,13 @@ func BenchmarkInitialWorkspaceLoad(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- doIWL(b, sharedEnv.Sandbox.GOPATH(), repo, test.file)
+ doIWL(b, sharedEnv.Sandbox.GOPATH(), repo)
}
})
}
}
-func doIWL(b *testing.B, gopath string, repo *repo, file string) {
+func doIWL(b *testing.B, gopath string, repo *repo) {
// Exclude the time to set up the env from the benchmark time, as this may
// involve installing gopls and/or checking out the repo dir.
b.StopTimer()
diff --git a/gopls/internal/test/integration/bench/stress_test.go b/gopls/internal/test/integration/bench/stress_test.go
index af72739875d..4ec272f5002 100644
--- a/gopls/internal/test/integration/bench/stress_test.go
+++ b/gopls/internal/test/integration/bench/stress_test.go
@@ -11,9 +11,9 @@ import (
"testing"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/lsprpc"
+ "golang.org/x/tools/gopls/internal/lsprpc"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/jsonrpc2/servertest"
diff --git a/gopls/internal/test/integration/bench/typing_test.go b/gopls/internal/test/integration/bench/typing_test.go
index 2d8aed62720..78bd16cef5b 100644
--- a/gopls/internal/test/integration/bench/typing_test.go
+++ b/gopls/internal/test/integration/bench/typing_test.go
@@ -10,7 +10,7 @@ import (
"testing"
"time"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// BenchmarkTyping simulates typing steadily in a single file at different
diff --git a/gopls/internal/test/integration/codelens/codelens_test.go b/gopls/internal/test/integration/codelens/codelens_test.go
index d0eecdca223..07ad3b9431b 100644
--- a/gopls/internal/test/integration/codelens/codelens_test.go
+++ b/gopls/internal/test/integration/codelens/codelens_test.go
@@ -14,8 +14,8 @@ import (
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/util/bug"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/internal/testenv"
)
diff --git a/gopls/internal/test/integration/codelens/gcdetails_test.go b/gopls/internal/test/integration/codelens/gcdetails_test.go
index 486f865d654..4d3024defe5 100644
--- a/gopls/internal/test/integration/codelens/gcdetails_test.go
+++ b/gopls/internal/test/integration/codelens/gcdetails_test.go
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/server"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
diff --git a/gopls/internal/test/integration/completion/completion18_test.go b/gopls/internal/test/integration/completion/completion18_test.go
index 8e10707f7e9..0ca83778664 100644
--- a/gopls/internal/test/integration/completion/completion18_test.go
+++ b/gopls/internal/test/integration/completion/completion18_test.go
@@ -10,7 +10,7 @@ package completion
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/completion/completion_test.go b/gopls/internal/test/integration/completion/completion_test.go
index 3c50a0c6f0a..b8d866b6263 100644
--- a/gopls/internal/test/integration/completion/completion_test.go
+++ b/gopls/internal/test/integration/completion/completion_test.go
@@ -13,7 +13,7 @@ import (
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/gopls/internal/util/bug"
diff --git a/gopls/internal/test/integration/debug/debug_test.go b/gopls/internal/test/integration/debug/debug_test.go
index 9aba51f37e5..255a8e1b90d 100644
--- a/gopls/internal/test/integration/debug/debug_test.go
+++ b/gopls/internal/test/integration/debug/debug_test.go
@@ -13,8 +13,8 @@ import (
"testing"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/util/bug"
)
@@ -65,7 +65,7 @@ func TestStartDebugging(t *testing.T) {
if err != nil {
t.Fatalf("reading HTTP response body: %v", err)
}
- const want = "GoPls"
+ const want = "Gopls"
if !strings.Contains(string(data), want) {
t.Errorf("GET %s response does not contain %q: <<%s>>", debugURL, want, data)
}
diff --git a/gopls/internal/test/integration/diagnostics/analysis_test.go b/gopls/internal/test/integration/diagnostics/analysis_test.go
index 403dc1657d8..8cb86f8f735 100644
--- a/gopls/internal/test/integration/diagnostics/analysis_test.go
+++ b/gopls/internal/test/integration/diagnostics/analysis_test.go
@@ -8,8 +8,8 @@ import (
"fmt"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go
index 81720e743d7..dba9532dd6d 100644
--- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go
+++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go
@@ -11,7 +11,7 @@ import (
"testing"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/server"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
@@ -1672,12 +1672,17 @@ const B = a.B
env.OpenFile("b/b.go")
env.AfterChange(
// The Go command sometimes tells us about only one of the import cycle
- // errors below. For robustness of this test, succeed if we get either.
+ // errors below. Also, sometimes we get an error during type checking
+ // instead of during list, due to missing metadata. This is likely due to
+ // a race.
+ // For robustness of this test, succeed if we get any reasonable error.
//
// TODO(golang/go#52904): we should get *both* of these errors.
+ // TODO(golang/go#64899): we should always get an import cycle error
+ // rather than a missing metadata error.
AnyOf(
- Diagnostics(env.AtRegexp("a/a.go", `"mod.test/b"`), WithMessage("import cycle")),
- Diagnostics(env.AtRegexp("b/b.go", `"mod.test/a"`), WithMessage("import cycle")),
+ Diagnostics(env.AtRegexp("a/a.go", `"mod.test/b"`)),
+ Diagnostics(env.AtRegexp("b/b.go", `"mod.test/a"`)),
),
)
env.RegexpReplace("b/b.go", `const B = a\.B`, "")
diff --git a/gopls/internal/test/integration/diagnostics/golist_test.go b/gopls/internal/test/integration/diagnostics/golist_test.go
index 91b9848df3f..8c11246d3e1 100644
--- a/gopls/internal/test/integration/diagnostics/golist_test.go
+++ b/gopls/internal/test/integration/diagnostics/golist_test.go
@@ -7,7 +7,7 @@ package diagnostics
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/cache"
+ "golang.org/x/tools/gopls/internal/cache"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/internal/testenv"
)
diff --git a/gopls/internal/test/integration/diagnostics/invalidation_test.go b/gopls/internal/test/integration/diagnostics/invalidation_test.go
index 795ec809be9..395e7619c57 100644
--- a/gopls/internal/test/integration/diagnostics/invalidation_test.go
+++ b/gopls/internal/test/integration/diagnostics/invalidation_test.go
@@ -8,7 +8,7 @@ import (
"fmt"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/diagnostics/undeclared_test.go b/gopls/internal/test/integration/diagnostics/undeclared_test.go
index e6e826fbf64..5579c0752d7 100644
--- a/gopls/internal/test/integration/diagnostics/undeclared_test.go
+++ b/gopls/internal/test/integration/diagnostics/undeclared_test.go
@@ -7,7 +7,7 @@ package diagnostics
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/env.go b/gopls/internal/test/integration/env.go
index 7c290ab5c02..8dab7d72873 100644
--- a/gopls/internal/test/integration/env.go
+++ b/gopls/internal/test/integration/env.go
@@ -11,7 +11,7 @@ import (
"sync"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/jsonrpc2/servertest"
)
diff --git a/gopls/internal/test/integration/env_test.go b/gopls/internal/test/integration/env_test.go
index 31a7ebfc55a..02bacd0f3db 100644
--- a/gopls/internal/test/integration/env_test.go
+++ b/gopls/internal/test/integration/env_test.go
@@ -9,7 +9,7 @@ import (
"encoding/json"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
func TestProgressUpdating(t *testing.T) {
diff --git a/gopls/internal/test/integration/expectation.go b/gopls/internal/test/integration/expectation.go
index eee7473dc22..c4f91c81af6 100644
--- a/gopls/internal/test/integration/expectation.go
+++ b/gopls/internal/test/integration/expectation.go
@@ -11,7 +11,7 @@ import (
"strings"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/server"
)
diff --git a/gopls/internal/test/integration/fake/client.go b/gopls/internal/test/integration/fake/client.go
index 127a3fa73f3..f940821eefe 100644
--- a/gopls/internal/test/integration/fake/client.go
+++ b/gopls/internal/test/integration/fake/client.go
@@ -8,8 +8,10 @@ import (
"context"
"encoding/json"
"fmt"
+ "path"
+ "path/filepath"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration/fake/glob"
)
@@ -97,9 +99,12 @@ 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.ScopeURI != nil && *item.ScopeURI == "" {
+ return nil, fmt.Errorf(`malformed ScopeURI ""`)
+ }
if item.Section == "gopls" {
config := c.editor.Config()
- results[i] = makeSettings(c.editor.sandbox, config)
+ results[i] = makeSettings(c.editor.sandbox, config, item.ScopeURI)
}
}
return results, nil
@@ -130,8 +135,15 @@ func (c *Client) RegisterCapability(ctx context.Context, params *protocol.Regist
}
var globs []*glob.Glob
for _, watcher := range opts.Watchers {
+ var globPattern string
+ switch pattern := watcher.GlobPattern.Value.(type) {
+ case protocol.Pattern:
+ globPattern = pattern
+ case protocol.RelativePattern:
+ globPattern = path.Join(filepath.ToSlash(pattern.BaseURI.Path()), pattern.Pattern)
+ }
// TODO(rfindley): honor the watch kind.
- g, err := glob.Parse(watcher.GlobPattern)
+ g, err := glob.Parse(globPattern)
if err != nil {
return fmt.Errorf("error parsing glob pattern %q: %v", watcher.GlobPattern, err)
}
diff --git a/gopls/internal/test/integration/fake/edit.go b/gopls/internal/test/integration/fake/edit.go
index 93fe2369589..b06984b3dbc 100644
--- a/gopls/internal/test/integration/fake/edit.go
+++ b/gopls/internal/test/integration/fake/edit.go
@@ -5,7 +5,7 @@
package fake
import (
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/diff"
)
diff --git a/gopls/internal/test/integration/fake/edit_test.go b/gopls/internal/test/integration/fake/edit_test.go
index 97e2c73e42d..0d7ac18c414 100644
--- a/gopls/internal/test/integration/fake/edit_test.go
+++ b/gopls/internal/test/integration/fake/edit_test.go
@@ -7,7 +7,7 @@ package fake
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
func TestApplyEdits(t *testing.T) {
diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go
index 6958564cd06..7c15106603f 100644
--- a/gopls/internal/test/integration/fake/editor.go
+++ b/gopls/internal/test/integration/fake/editor.go
@@ -17,10 +17,11 @@ 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/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/test/integration/fake/glob"
"golang.org/x/tools/gopls/internal/util/pathutil"
+ "golang.org/x/tools/gopls/internal/util/slices"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/jsonrpc2/servertest"
"golang.org/x/tools/internal/xcontext"
@@ -72,7 +73,7 @@ func (b buffer) text() string {
}
// EditorConfig configures the editor's LSP session. This is similar to
-// source.UserOptions, but we use a separate type here so that we expose only
+// golang.UserOptions, but we use a separate type here so that we expose only
// that configuration which we support.
//
// The zero value for EditorConfig is the default configuration.
@@ -109,7 +110,13 @@ type EditorConfig struct {
FileAssociations map[string]string
// Settings holds user-provided configuration for the LSP server.
- Settings map[string]interface{}
+ Settings map[string]any
+
+ // FolderSettings holds user-provided per-folder configuration, if any.
+ //
+ // It maps each folder (as a relative path to the sandbox workdir) to its
+ // configuration mapping (like Settings).
+ FolderSettings map[string]map[string]any
// CapabilitiesJSON holds JSON client capabilities to overlay over the
// editor's default client capabilities.
@@ -215,7 +222,7 @@ func (e *Editor) Client() *Client {
}
// makeSettings builds the settings map for use in LSP settings RPCs.
-func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{} {
+func makeSettings(sandbox *Sandbox, config EditorConfig, scopeURI *protocol.URI) map[string]any {
env := make(map[string]string)
for k, v := range sandbox.GoEnv() {
env[k] = v
@@ -228,7 +235,7 @@ func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{}
env[k] = v
}
- settings := map[string]interface{}{
+ settings := map[string]any{
"env": env,
// Use verbose progress reporting so that integration tests can assert on
@@ -247,6 +254,28 @@ func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{}
settings[k] = v
}
+ // If the server is requesting configuration for a specific scope, apply
+ // settings for the nearest folder that has customized settings, if any.
+ if scopeURI != nil {
+ var (
+ scopePath = protocol.DocumentURI(*scopeURI).Path()
+ closestDir string // longest dir with settings containing the scope, if any
+ closestSettings map[string]any // settings for that dir, if any
+ )
+ for relPath, settings := range config.FolderSettings {
+ dir := sandbox.Workdir.AbsPath(relPath)
+ if strings.HasPrefix(scopePath+string(filepath.Separator), dir+string(filepath.Separator)) && len(dir) > len(closestDir) {
+ closestDir = dir
+ closestSettings = settings
+ }
+ }
+ if closestSettings != nil {
+ for k, v := range closestSettings {
+ settings[k] = v
+ }
+ }
+ }
+
return settings
}
@@ -260,45 +289,14 @@ func (e *Editor) initialize(ctx context.Context) error {
Version: "v1.0.0",
}
}
- params.InitializationOptions = makeSettings(e.sandbox, config)
+ params.InitializationOptions = makeSettings(e.sandbox, config, nil)
params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders)
- // Set various client capabilities that are sought by gopls.
- params.Capabilities.Workspace.Configuration = true // support workspace/configuration
- params.Capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress
- params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport = &protocol.CompletionItemTagOptions{}
- params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
- params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
- params.Capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true}
- params.Capabilities.TextDocument.SemanticTokens.TokenTypes = []string{
- "namespace", "type", "class", "enum", "interface",
- "struct", "typeParameter", "parameter", "variable", "property", "enumMember",
- "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",
- }
- // The LSP tests have historically enabled this flag,
- // but really we should test both ways for older editors.
- params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true
- // Glob pattern watching is enabled.
- params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
- // "rename" operations are used for package renaming.
- //
- // TODO(rfindley): add support for other resource operations (create, delete, ...)
- params.Capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{
- ResourceOperations: []protocol.ResourceOperationKind{
- "rename",
- },
- }
- // Apply capabilities overlay.
- if config.CapabilitiesJSON != nil {
- if err := json.Unmarshal(config.CapabilitiesJSON, ¶ms.Capabilities); err != nil {
- return fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err)
- }
+ capabilities, err := clientCapabilities(config)
+ if err != nil {
+ return fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err)
}
+ params.Capabilities = capabilities
trace := protocol.TraceValues("messages")
params.Trace = &trace
@@ -325,6 +323,48 @@ func (e *Editor) initialize(ctx context.Context) error {
return nil
}
+func clientCapabilities(cfg EditorConfig) (protocol.ClientCapabilities, error) {
+ var capabilities protocol.ClientCapabilities
+ // Set various client capabilities that are sought by gopls.
+ capabilities.Workspace.Configuration = true // support workspace/configuration
+ capabilities.TextDocument.Completion.CompletionItem.TagSupport = &protocol.CompletionItemTagOptions{}
+ capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated}
+ capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
+ capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true}
+ capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress
+ capabilities.TextDocument.SemanticTokens.TokenTypes = []string{
+ "namespace", "type", "class", "enum", "interface",
+ "struct", "typeParameter", "parameter", "variable", "property", "enumMember",
+ "event", "function", "method", "macro", "keyword", "modifier", "comment",
+ "string", "number", "regexp", "operator",
+ }
+ capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{
+ "declaration", "definition", "readonly", "static",
+ "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary",
+ }
+ // The LSP tests have historically enabled this flag,
+ // but really we should test both ways for older editors.
+ capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true
+ // Glob pattern watching is enabled.
+ capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
+ // "rename" operations are used for package renaming.
+ //
+ // TODO(rfindley): add support for other resource operations (create, delete, ...)
+ capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{
+ ResourceOperations: []protocol.ResourceOperationKind{
+ "rename",
+ },
+ }
+
+ // Apply capabilities overlay.
+ if cfg.CapabilitiesJSON != nil {
+ if err := json.Unmarshal(cfg.CapabilitiesJSON, &capabilities); err != nil {
+ return protocol.ClientCapabilities{}, fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err)
+ }
+ }
+ return capabilities, nil
+}
+
// marshalUnmarshal is a helper to json Marshal and then Unmarshal as a
// different type. Used to work around cases where our protocol types are not
// specific.
@@ -902,6 +942,21 @@ func (e *Editor) ApplyQuickFixes(ctx context.Context, loc protocol.Location, dia
// ApplyCodeAction applies the given code action.
func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction) error {
+ // Resolve the code actions if necessary and supported.
+ if action.Edit == nil {
+ editSupport, err := e.EditResolveSupport()
+ if err != nil {
+ return err
+ }
+ if editSupport {
+ ca, err := e.Server.ResolveCodeAction(ctx, &action)
+ if err != nil {
+ return err
+ }
+ action.Edit = ca.Edit
+ }
+ }
+
if action.Edit != nil {
for _, change := range action.Edit.DocumentChanges {
if change.TextDocumentEdit != nil {
@@ -932,11 +987,11 @@ func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction
// GetQuickFixes returns the available quick fix code actions.
func (e *Editor) GetQuickFixes(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
- return e.getCodeActions(ctx, loc, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
+ return e.CodeActions(ctx, loc, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
}
func (e *Editor) applyCodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) (int, error) {
- actions, err := e.getCodeActions(ctx, loc, diagnostics, only...)
+ actions, err := e.CodeActions(ctx, loc, diagnostics, only...)
if err != nil {
return 0, err
}
@@ -963,7 +1018,7 @@ func (e *Editor) applyCodeActions(ctx context.Context, loc protocol.Location, di
return applied, nil
}
-func (e *Editor) getCodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) {
+func (e *Editor) CodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) {
if e.Server == nil {
return nil, nil
}
@@ -1264,7 +1319,7 @@ func (e *Editor) SignatureHelp(ctx context.Context, loc protocol.Location) (*pro
}
func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error {
- closed, opened, err := e.renameBuffers(ctx, oldPath, newPath)
+ closed, opened, err := e.renameBuffers(oldPath, newPath)
if err != nil {
return err
}
@@ -1290,7 +1345,7 @@ func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error
// 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) {
+func (e *Editor) renameBuffers(oldPath, newPath string) (closed []protocol.TextDocumentIdentifier, opened []protocol.TextDocumentItem, _ error) {
e.mu.Lock()
defer e.mu.Unlock()
@@ -1471,6 +1526,14 @@ func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnost
return lens, nil
}
+func (e *Editor) EditResolveSupport() (bool, error) {
+ capabilities, err := clientCapabilities(e.Config())
+ if err != nil {
+ return false, err
+ }
+ return capabilities.TextDocument.CodeAction.ResolveSupport != nil && slices.Contains(capabilities.TextDocument.CodeAction.ResolveSupport.Properties, "edit"), nil
+}
+
// Hover triggers a hover at the given position in an open buffer.
func (e *Editor) Hover(ctx context.Context, loc protocol.Location) (*protocol.MarkupContent, protocol.Location, error) {
if err := e.checkBufferLocation(loc); err != nil {
diff --git a/gopls/internal/test/integration/fake/editor_test.go b/gopls/internal/test/integration/fake/editor_test.go
index cc8a14744d2..68983bda50c 100644
--- a/gopls/internal/test/integration/fake/editor_test.go
+++ b/gopls/internal/test/integration/fake/editor_test.go
@@ -8,7 +8,7 @@ import (
"context"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
const exampleProgram = `
diff --git a/gopls/internal/test/integration/fake/workdir.go b/gopls/internal/test/integration/fake/workdir.go
index 3e18e97a5a6..4d21554d4a8 100644
--- a/gopls/internal/test/integration/fake/workdir.go
+++ b/gopls/internal/test/integration/fake/workdir.go
@@ -18,7 +18,7 @@ import (
"sync"
"time"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/robustio"
)
diff --git a/gopls/internal/test/integration/fake/workdir_test.go b/gopls/internal/test/integration/fake/workdir_test.go
index b45b5339991..153a3576b4e 100644
--- a/gopls/internal/test/integration/fake/workdir_test.go
+++ b/gopls/internal/test/integration/fake/workdir_test.go
@@ -11,7 +11,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
const sharedData = `
diff --git a/gopls/internal/test/integration/inlayhints/inlayhints_test.go b/gopls/internal/test/integration/inlayhints/inlayhints_test.go
index c56fae500e3..eab430f23bb 100644
--- a/gopls/internal/test/integration/inlayhints/inlayhints_test.go
+++ b/gopls/internal/test/integration/inlayhints/inlayhints_test.go
@@ -6,9 +6,9 @@ package inlayhint
import (
"testing"
+ "golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/hooks"
. "golang.org/x/tools/gopls/internal/test/integration"
- "golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/util/bug"
)
@@ -42,12 +42,12 @@ const (
},
{
label: "enable const",
- enabled: map[string]bool{source.ConstantValues: true},
+ enabled: map[string]bool{golang.ConstantValues: true},
wantInlayHint: true,
},
{
label: "enable parameter names",
- enabled: map[string]bool{source.ParameterNames: true},
+ enabled: map[string]bool{golang.ParameterNames: true},
wantInlayHint: false,
},
}
diff --git a/gopls/internal/test/integration/misc/call_hierarchy_test.go b/gopls/internal/test/integration/misc/call_hierarchy_test.go
index 4587e41bc9a..ebe4d555811 100644
--- a/gopls/internal/test/integration/misc/call_hierarchy_test.go
+++ b/gopls/internal/test/integration/misc/call_hierarchy_test.go
@@ -6,7 +6,7 @@ package misc
import (
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/misc/configuration_test.go b/gopls/internal/test/integration/misc/configuration_test.go
index c8cdc5334c5..fff4576bc5b 100644
--- a/gopls/internal/test/integration/misc/configuration_test.go
+++ b/gopls/internal/test/integration/misc/configuration_test.go
@@ -18,7 +18,7 @@ func TestChangeConfiguration(t *testing.T) {
// Staticcheck only supports Go versions >= 1.19.
// Note: keep this in sync with TestStaticcheckWarning. Below this version we
// should get an error when setting staticcheck configuration.
- testenv.NeedsGo1Point(t, 19)
+ testenv.NeedsGo1Point(t, 20)
const files = `
-- go.mod --
@@ -49,13 +49,55 @@ var FooErr = errors.New("foo")
})
}
+// Test that clients can configure per-workspace configuration, which is
+// queried via the scopeURI of a workspace/configuration request.
+// (this was broken in golang/go#65519).
+func TestWorkspaceConfiguration(t *testing.T) {
+ const files = `
+-- go.mod --
+module example.com/config
+
+go 1.18
+
+-- a/a.go --
+package a
+
+import "example.com/config/b"
+
+func _() {
+ _ = b.B{2}
+}
+
+-- b/b.go --
+package b
+
+type B struct {
+ F int
+}
+`
+
+ WithOptions(
+ WorkspaceFolders("a"),
+ FolderSettings(map[string]Settings{
+ "a": {
+ "analyses": map[string]bool{
+ "composites": false,
+ },
+ },
+ }),
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+ env.AfterChange(NoDiagnostics())
+ })
+}
+
// TestMajorOptionsChange is like TestChangeConfiguration, but modifies an
// an open buffer before making a major (but inconsequential) change that
// causes gopls to recreate the view.
//
// Gopls should not get confused about buffer content when recreating the view.
func TestMajorOptionsChange(t *testing.T) {
- testenv.NeedsGo1Point(t, 19) // needs staticcheck
+ testenv.NeedsGo1Point(t, 20) // needs staticcheck
const files = `
-- go.mod --
@@ -96,7 +138,7 @@ var ErrFoo = errors.New("foo")
func TestStaticcheckWarning(t *testing.T) {
// Note: keep this in sync with TestChangeConfiguration.
- testenv.SkipAfterGo1Point(t, 16)
+ testenv.SkipAfterGo1Point(t, 19)
const files = `
-- go.mod --
@@ -142,6 +184,7 @@ func TestDeprecatedSettings(t *testing.T) {
"experimentalWatchedFileDelay": "1s",
"experimentalWorkspaceModule": true,
"tempModfile": true,
+ "allowModfileModifications": true,
},
).Run(t, "", func(t *testing.T, env *Env) {
env.OnceMet(
@@ -150,6 +193,7 @@ func TestDeprecatedSettings(t *testing.T) {
ShownMessage("experimentalUseInvalidMetadata"),
ShownMessage("experimentalWatchedFileDelay"),
ShownMessage("tempModfile"),
+ ShownMessage("allowModfileModifications"),
)
})
}
diff --git a/gopls/internal/test/integration/misc/debugserver_test.go b/gopls/internal/test/integration/misc/debugserver_test.go
index b9c684f61ed..d1ce21bd47a 100644
--- a/gopls/internal/test/integration/misc/debugserver_test.go
+++ b/gopls/internal/test/integration/misc/debugserver_test.go
@@ -8,8 +8,8 @@ import (
"net/http"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/misc/definition_test.go b/gopls/internal/test/integration/misc/definition_test.go
index 9a6d1af8d54..b7394b8dd67 100644
--- a/gopls/internal/test/integration/misc/definition_test.go
+++ b/gopls/internal/test/integration/misc/definition_test.go
@@ -11,9 +11,9 @@ import (
"strings"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- . "golang.org/x/tools/gopls/internal/test/integration"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/compare"
+ . "golang.org/x/tools/gopls/internal/test/integration"
)
const internalDefinition = `
diff --git a/gopls/internal/test/integration/misc/extract_test.go b/gopls/internal/test/integration/misc/extract_test.go
index d2be705ddfa..5d6c2c5f214 100644
--- a/gopls/internal/test/integration/misc/extract_test.go
+++ b/gopls/internal/test/integration/misc/extract_test.go
@@ -6,10 +6,10 @@ package misc
import (
"testing"
- . "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/compare"
+ . "golang.org/x/tools/gopls/internal/test/integration"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
func TestExtractFunction(t *testing.T) {
diff --git a/gopls/internal/test/integration/misc/fix_test.go b/gopls/internal/test/integration/misc/fix_test.go
index 81b855936e1..9daf5addf87 100644
--- a/gopls/internal/test/integration/misc/fix_test.go
+++ b/gopls/internal/test/integration/misc/fix_test.go
@@ -7,14 +7,24 @@ package misc
import (
"testing"
- . "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/compare"
+ . "golang.org/x/tools/gopls/internal/test/integration"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
-// A basic test for fillstruct, now that it uses a command.
+// A basic test for fillstruct, now that it uses a command and supports resolve edits.
func TestFillStruct(t *testing.T) {
+ tc := []struct {
+ name string
+ capabilities string
+ wantCommand bool
+ }{
+ {"default", "{}", true},
+ {"no data", `{ "textDocument": {"codeAction": { "resolveSupport": { "properties": ["edit"] } } } }`, true},
+ {"resolve support", `{ "textDocument": {"codeAction": { "dataSupport": true, "resolveSupport": { "properties": ["edit"] } } } }`, false},
+ }
+
const basic = `
-- go.mod --
module mod.com
@@ -32,12 +42,35 @@ func Foo() {
_ = Info{}
}
`
- Run(t, basic, func(t *testing.T, env *Env) {
- env.OpenFile("main.go")
- if err := env.Editor.RefactorRewrite(env.Ctx, env.RegexpSearch("main.go", "Info{}")); err != nil {
- t.Fatal(err)
- }
- want := `package main
+
+ for _, tt := range tc {
+ t.Run(tt.name, func(t *testing.T) {
+ runner := WithOptions(CapabilitiesJSON([]byte(tt.capabilities)))
+
+ runner.Run(t, basic, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ fixes, err := env.Editor.CodeActions(env.Ctx, env.RegexpSearch("main.go", "Info{}"), nil, protocol.RefactorRewrite)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(fixes) != 1 {
+ t.Fatalf("expected 1 code action, got %v", len(fixes))
+ }
+ if tt.wantCommand {
+ if fixes[0].Command == nil || fixes[0].Data != nil {
+ t.Errorf("expected code action to have command not data, got %v", fixes[0])
+ }
+ } else {
+ if fixes[0].Command != nil || fixes[0].Data == nil {
+ t.Errorf("expected code action to have command not data, got %v", fixes[0])
+ }
+ }
+
+ // Apply the code action (handles resolving the code action), and check that the result is correct.
+ if err := env.Editor.RefactorRewrite(env.Ctx, env.RegexpSearch("main.go", "Info{}")); err != nil {
+ t.Fatal(err)
+ }
+ want := `package main
type Info struct {
WordCounts map[string]int
@@ -51,10 +84,12 @@ func Foo() {
}
}
`
- if got := env.BufferText("main.go"); got != want {
- t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got))
- }
- })
+ if got := env.BufferText("main.go"); got != want {
+ t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got))
+ }
+ })
+ })
+ }
}
func TestFillReturns(t *testing.T) {
@@ -79,24 +114,18 @@ func Foo() error {
ReadDiagnostics("main.go", &d),
)
codeActions := env.CodeAction("main.go", d.Diagnostics)
- if len(codeActions) != 2 {
- t.Fatalf("expected 2 code actions, got %v", len(codeActions))
+ if len(codeActions) != 1 {
+ t.Fatalf("expected 1 code actions, got %v\n%v", len(codeActions), codeActions)
}
- var foundQuickFix, foundFixAll bool
+ var foundQuickFix bool
for _, a := range codeActions {
if a.Kind == protocol.QuickFix {
foundQuickFix = true
}
- if a.Kind == protocol.SourceFixAll {
- foundFixAll = true
- }
}
if !foundQuickFix {
t.Fatalf("expected quickfix code action, got none")
}
- if !foundFixAll {
- t.Fatalf("expected fixall code action, got none")
- }
env.ApplyQuickFixes("main.go", d.Diagnostics)
env.AfterChange(NoDiagnostics(ForFile("main.go")))
})
diff --git a/gopls/internal/test/integration/misc/formatting_test.go b/gopls/internal/test/integration/misc/formatting_test.go
index 8fb3ac6f90c..1808dbc8791 100644
--- a/gopls/internal/test/integration/misc/formatting_test.go
+++ b/gopls/internal/test/integration/misc/formatting_test.go
@@ -8,8 +8,8 @@ import (
"strings"
"testing"
- . "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/compare"
+ . "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/internal/testenv"
)
@@ -303,6 +303,7 @@ func main() {
}
func TestGofumptFormatting(t *testing.T) {
+ testenv.NeedsGo1Point(t, 20) // gofumpt requires go 1.20+
// Exercise some gofumpt formatting rules:
// - No empty lines following an assignment operator
// - Octal integer literals should use the 0o prefix on modules using Go
diff --git a/gopls/internal/test/integration/misc/highlight_test.go b/gopls/internal/test/integration/misc/highlight_test.go
index 9d992fb7368..9e3dd980464 100644
--- a/gopls/internal/test/integration/misc/highlight_test.go
+++ b/gopls/internal/test/integration/misc/highlight_test.go
@@ -8,7 +8,7 @@ import (
"sort"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/misc/hover_test.go b/gopls/internal/test/integration/misc/hover_test.go
index d246814e9d9..3853938f12f 100644
--- a/gopls/internal/test/integration/misc/hover_test.go
+++ b/gopls/internal/test/integration/misc/hover_test.go
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/testenv"
@@ -491,3 +491,26 @@ func TestHoverEmbedDirective(t *testing.T) {
}
})
}
+
+func TestHoverBrokenImport_Issue60592(t *testing.T) {
+ const files = `
+-- go.mod --
+module testdata
+go 1.18
+
+-- p.go --
+package main
+
+import foo "a"
+
+func _() {
+ foo.Print()
+}
+
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("p.go")
+ // This request should not crash gopls.
+ _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("p.go", "foo[.]"))
+ })
+}
diff --git a/gopls/internal/test/integration/misc/import_test.go b/gopls/internal/test/integration/misc/import_test.go
index 5e467924b29..0df3f8dadec 100644
--- a/gopls/internal/test/integration/misc/import_test.go
+++ b/gopls/internal/test/integration/misc/import_test.go
@@ -8,10 +8,10 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- . "golang.org/x/tools/gopls/internal/test/integration"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/test/compare"
+ . "golang.org/x/tools/gopls/internal/test/integration"
)
func TestAddImport(t *testing.T) {
diff --git a/gopls/internal/test/integration/misc/imports_test.go b/gopls/internal/test/integration/misc/imports_test.go
index 4fa7d9d07e3..34ccaf7aa67 100644
--- a/gopls/internal/test/integration/misc/imports_test.go
+++ b/gopls/internal/test/integration/misc/imports_test.go
@@ -10,10 +10,10 @@ import (
"strings"
"testing"
- . "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/compare"
+ . "golang.org/x/tools/gopls/internal/test/integration"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
// Tests golang/go#38815.
diff --git a/gopls/internal/test/integration/misc/misc_test.go b/gopls/internal/test/integration/misc/misc_test.go
index bca00bd9831..1567044caef 100644
--- a/gopls/internal/test/integration/misc/misc_test.go
+++ b/gopls/internal/test/integration/misc/misc_test.go
@@ -9,7 +9,7 @@ import (
"testing"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/util/bug"
diff --git a/gopls/internal/test/integration/misc/prompt_test.go b/gopls/internal/test/integration/misc/prompt_test.go
index e2c4f1b524b..26c0e9322ac 100644
--- a/gopls/internal/test/integration/misc/prompt_test.go
+++ b/gopls/internal/test/integration/misc/prompt_test.go
@@ -11,10 +11,10 @@ import (
"regexp"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- . "golang.org/x/tools/gopls/internal/test/integration"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/server"
+ . "golang.org/x/tools/gopls/internal/test/integration"
)
// Test that gopls prompts for telemetry only when it is supposed to.
diff --git a/gopls/internal/test/integration/misc/references_test.go b/gopls/internal/test/integration/misc/references_test.go
index df198045cb0..fcd72d85c68 100644
--- a/gopls/internal/test/integration/misc/references_test.go
+++ b/gopls/internal/test/integration/misc/references_test.go
@@ -14,7 +14,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/misc/rename_test.go b/gopls/internal/test/integration/misc/rename_test.go
index fe23448e7d2..e3116e1dd2a 100644
--- a/gopls/internal/test/integration/misc/rename_test.go
+++ b/gopls/internal/test/integration/misc/rename_test.go
@@ -9,9 +9,9 @@ import (
"strings"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
- . "golang.org/x/tools/gopls/internal/test/integration"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/compare"
+ . "golang.org/x/tools/gopls/internal/test/integration"
)
func TestPrepareRenameMainPackage(t *testing.T) {
diff --git a/gopls/internal/test/integration/misc/semantictokens_test.go b/gopls/internal/test/integration/misc/semantictokens_test.go
index b0b49ec032b..96d35bf74f1 100644
--- a/gopls/internal/test/integration/misc/semantictokens_test.go
+++ b/gopls/internal/test/integration/misc/semantictokens_test.go
@@ -10,7 +10,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
)
@@ -193,3 +193,47 @@ func bar() {}
}
})
}
+
+// Make sure no zero-length tokens occur
+func TestSemantic_65254(t *testing.T) {
+ src := `
+-- go.mod --
+module example.com
+
+go 1.21
+-- main.go --
+package main
+
+/* a comment with an
+
+empty line
+*/
+
+const bad = `
+
+ src += "`foo" + `
+ ` + "bar`"
+ want := []fake.SemanticToken{
+ {Token: "package", TokenType: "keyword"},
+ {Token: "main", TokenType: "namespace"},
+ {Token: "/* a comment with an", TokenType: "comment"},
+ // --- Note that the zero length line does not show up
+ {Token: "empty line", TokenType: "comment"},
+ {Token: "*/", TokenType: "comment"},
+ {Token: "const", TokenType: "keyword"},
+ {Token: "bad", TokenType: "variable", Mod: "definition readonly"},
+ {Token: "`foo", TokenType: "string"},
+ // --- Note the zero length line does not show up
+ {Token: "\tbar`", TokenType: "string"},
+ }
+ WithOptions(
+ Modes(Default),
+ Settings{"semanticTokens": true},
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ seen := env.SemanticTokensFull("main.go")
+ if x := cmp.Diff(want, seen); x != "" {
+ t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
+ }
+ })
+}
diff --git a/gopls/internal/test/integration/misc/signature_help_test.go b/gopls/internal/test/integration/misc/signature_help_test.go
index dcfb416f292..8dffedf48e0 100644
--- a/gopls/internal/test/integration/misc/signature_help_test.go
+++ b/gopls/internal/test/integration/misc/signature_help_test.go
@@ -8,7 +8,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/misc/staticcheck_test.go b/gopls/internal/test/integration/misc/staticcheck_test.go
index 535458e925f..6c7a156f189 100644
--- a/gopls/internal/test/integration/misc/staticcheck_test.go
+++ b/gopls/internal/test/integration/misc/staticcheck_test.go
@@ -13,7 +13,7 @@ import (
)
func TestStaticcheckGenerics(t *testing.T) {
- testenv.NeedsGo1Point(t, 19) // generics were introduced in Go 1.18, staticcheck requires go1.19+
+ testenv.NeedsGo1Point(t, 20) // staticcheck requires go1.20+
const files = `
-- go.mod --
@@ -78,7 +78,7 @@ 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, 19) // staticcheck is only supported at Go 1.19+
+ testenv.NeedsGo1Point(t, 20) // staticcheck is only supported at Go 1.20+
const files = `
-- go.mod --
module mod.test
diff --git a/gopls/internal/test/integration/misc/vendor_test.go b/gopls/internal/test/integration/misc/vendor_test.go
index 18b08f616bb..f3bed9082b7 100644
--- a/gopls/internal/test/integration/misc/vendor_test.go
+++ b/gopls/internal/test/integration/misc/vendor_test.go
@@ -9,7 +9,7 @@ import (
. "golang.org/x/tools/gopls/internal/test/integration"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
const basicProxy = `
@@ -51,8 +51,7 @@ func _() {
).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) {
env.OpenFile("a/a1.go")
d := &protocol.PublishDiagnosticsParams{}
- env.OnceMet(
- InitialWorkspaceLoad,
+ env.AfterChange(
Diagnostics(env.AtRegexp("go.mod", "module mod.com"), WithMessage("Inconsistent vendoring")),
ReadDiagnostics("go.mod", d),
)
diff --git a/gopls/internal/test/integration/misc/vuln_test.go b/gopls/internal/test/integration/misc/vuln_test.go
index a0d260cf43d..f74c1c38d77 100644
--- a/gopls/internal/test/integration/misc/vuln_test.go
+++ b/gopls/internal/test/integration/misc/vuln_test.go
@@ -17,9 +17,9 @@ import (
"github.com/google/go-cmp/cmp"
- "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/cache"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/test/compare"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/vulncheck"
@@ -434,7 +434,7 @@ func (v VulnData) Vuln1() {}
func (v VulnData) Vuln2() {}
`
-func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) {
+func vulnTestEnv(proxyData string) (*vulntest.DB, []RunOption, error) {
db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData))
if err != nil {
return nil, nil, nil
@@ -458,7 +458,7 @@ func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) {
}
func TestRunVulncheckPackageDiagnostics(t *testing.T) {
- db, opts0, err := vulnTestEnv(vulnsData, proxy1)
+ db, opts0, err := vulnTestEnv(proxy1)
if err != nil {
t.Fatal(err)
}
@@ -606,7 +606,7 @@ func TestRunGovulncheck_Expiry(t *testing.T) {
}(cache.MaxGovulncheckResultAge)
cache.MaxGovulncheckResultAge = 99 * time.Millisecond
- db, opts0, err := vulnTestEnv(vulnsData, proxy1)
+ db, opts0, err := vulnTestEnv(proxy1)
if err != nil {
t.Fatal(err)
}
@@ -638,7 +638,7 @@ func stringify(a interface{}) string {
}
func TestRunVulncheckWarning(t *testing.T) {
- db, opts, err := vulnTestEnv(vulnsData, proxy1)
+ db, opts, err := vulnTestEnv(proxy1)
if err != nil {
t.Fatal(err)
}
@@ -793,7 +793,7 @@ func OK() {} // ok.
`
func TestGovulncheckInfo(t *testing.T) {
- db, opts, err := vulnTestEnv(vulnsData, proxy2)
+ db, opts, err := vulnTestEnv(proxy2)
if err != nil {
t.Fatal(err)
}
diff --git a/gopls/internal/test/integration/modfile/modfile_test.go b/gopls/internal/test/integration/modfile/modfile_test.go
index 92d91b14ae2..a71caaebe97 100644
--- a/gopls/internal/test/integration/modfile/modfile_test.go
+++ b/gopls/internal/test/integration/modfile/modfile_test.go
@@ -15,7 +15,7 @@ import (
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/util/bug"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
)
func TestMain(m *testing.M) {
@@ -492,8 +492,8 @@ var _ = blah.Name
env.AfterChange(
// We would like for the error to appear in the v2 module, but
// as of writing non-workspace packages are not diagnosed.
- Diagnostics(env.AtRegexp("a/main.go", `"example.com/blah/v2"`), WithMessage("cannot find module providing")),
- Diagnostics(env.AtRegexp("a/go.mod", `require example.com/blah/v2`), WithMessage("cannot find module providing")),
+ Diagnostics(env.AtRegexp("a/main.go", `"example.com/blah/v2"`), WithMessage("no required module provides")),
+ Diagnostics(env.AtRegexp("a/go.mod", `require example.com/blah/v2`), WithMessage("no required module provides")),
ReadDiagnostics("a/go.mod", &modDiags),
)
diff --git a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go
index ded09b47c18..a6c394e3467 100644
--- a/gopls/internal/test/integration/options.go
+++ b/gopls/internal/test/integration/options.go
@@ -5,7 +5,7 @@
package integration
import (
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration/fake"
)
@@ -73,6 +73,13 @@ func ClientName(name string) RunOption {
})
}
+// CapabilitiesJSON sets the capabalities json.
+func CapabilitiesJSON(capabilities []byte) RunOption {
+ return optionSetter(func(opts *runConfig) {
+ opts.editor.CapabilitiesJSON = capabilities
+ })
+}
+
// Settings sets user-provided configuration for the LSP server.
//
// As a special case, the env setting must not be provided via Settings: use
@@ -97,11 +104,29 @@ func WorkspaceFolders(relFolders ...string) RunOption {
// Use an empty non-nil slice to signal explicitly no folders.
relFolders = []string{}
}
+
return optionSetter(func(opts *runConfig) {
opts.editor.WorkspaceFolders = relFolders
})
}
+// FolderSettings defines per-folder workspace settings, keyed by relative path
+// to the folder.
+//
+// Use in conjunction with WorkspaceFolders to have different settings for
+// different folders.
+func FolderSettings(folderSettings map[string]Settings) RunOption {
+ // Re-use the Settings type, for symmetry, but translate back into maps for
+ // the editor config.
+ folders := make(map[string]map[string]any)
+ for k, v := range folderSettings {
+ folders[k] = v
+ }
+ return optionSetter(func(opts *runConfig) {
+ opts.editor.FolderSettings = folders
+ })
+}
+
// 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.
diff --git a/gopls/internal/test/integration/regtest.go b/gopls/internal/test/integration/regtest.go
index 560f520e1eb..4e26fd79d5b 100644
--- a/gopls/internal/test/integration/regtest.go
+++ b/gopls/internal/test/integration/regtest.go
@@ -13,8 +13,8 @@ import (
"testing"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cmd"
- "golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/memoize"
@@ -114,7 +114,7 @@ func Main(m *testing.M, hook func(*settings.Options)) {
// 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:])
+ tool.Main(context.Background(), cmd.New(hook), os.Args[1:])
os.Exit(0)
}
diff --git a/gopls/internal/test/integration/runner.go b/gopls/internal/test/integration/runner.go
index 39ca4793b21..7696b346b8f 100644
--- a/gopls/internal/test/integration/runner.go
+++ b/gopls/internal/test/integration/runner.go
@@ -20,10 +20,10 @@ import (
"testing"
"time"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/lsprpc"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsprpc"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/jsonrpc2"
@@ -182,7 +182,7 @@ func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOptio
}
// TODO(rfindley): do we need an instance at all? Can it be removed?
- ctx = debug.WithInstance(ctx, "", "off")
+ ctx = debug.WithInstance(ctx, "off")
rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name()))
if err := os.MkdirAll(rootDir, 0755); err != nil {
@@ -349,7 +349,7 @@ func (r *Runner) experimentalServer(optsHook func(*settings.Options)) jsonrpc2.S
func (r *Runner) forwardedServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer {
r.tsOnce.Do(func() {
ctx := context.Background()
- ctx = debug.WithInstance(ctx, "", "off")
+ ctx = debug.WithInstance(ctx, "off")
ss := lsprpc.NewStreamServer(cache.New(nil), false, optsHook)
r.ts = servertest.NewTCPServer(ctx, ss, nil)
})
@@ -403,7 +403,7 @@ func (r *Runner) separateProcessServer(optsHook func(*settings.Options)) jsonrpc
return newForwarder("unix", r.remoteSocket)
}
-func newForwarder(network, address string) *lsprpc.Forwarder {
+func newForwarder(network, address string) jsonrpc2.StreamServer {
server, err := lsprpc.NewForwarder(network+";"+address, nil)
if err != nil {
// This should never happen, as we are passing an explicit address.
diff --git a/gopls/internal/test/integration/template/template_test.go b/gopls/internal/test/integration/template/template_test.go
index ae6a3a6e8eb..28ea9182284 100644
--- a/gopls/internal/test/integration/template/template_test.go
+++ b/gopls/internal/test/integration/template/template_test.go
@@ -9,7 +9,7 @@ import (
"testing"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/util/bug"
)
diff --git a/gopls/internal/test/integration/watch/watch_test.go b/gopls/internal/test/integration/watch/watch_test.go
index c401d96079f..fab302ff149 100644
--- a/gopls/internal/test/integration/watch/watch_test.go
+++ b/gopls/internal/test/integration/watch/watch_test.go
@@ -11,7 +11,7 @@ import (
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/util/bug"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration/fake"
)
@@ -258,7 +258,7 @@ func _() {
}
// Add a new method to an interface and implement it.
-// Inspired by the structure of internal/lsp/source and internal/lsp/cache.
+// Inspired by the structure of internal/golang and internal/cache.
func TestCreateImplementation(t *testing.T) {
const pkg = `
-- go.mod --
diff --git a/gopls/internal/test/integration/workspace/quickfix_test.go b/gopls/internal/test/integration/workspace/quickfix_test.go
index 03042333be8..6f7c8e854d0 100644
--- a/gopls/internal/test/integration/workspace/quickfix_test.go
+++ b/gopls/internal/test/integration/workspace/quickfix_test.go
@@ -8,7 +8,7 @@ import (
"strings"
"testing"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/compare"
. "golang.org/x/tools/gopls/internal/test/integration"
diff --git a/gopls/internal/test/integration/workspace/standalone_test.go b/gopls/internal/test/integration/workspace/standalone_test.go
index e50ead350cd..d837899f7fb 100644
--- a/gopls/internal/test/integration/workspace/standalone_test.go
+++ b/gopls/internal/test/integration/workspace/standalone_test.go
@@ -9,7 +9,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
)
diff --git a/gopls/internal/test/integration/workspace/workspace_test.go b/gopls/internal/test/integration/workspace/workspace_test.go
index baad7bd002a..28b3978a8cd 100644
--- a/gopls/internal/test/integration/workspace/workspace_test.go
+++ b/gopls/internal/test/integration/workspace/workspace_test.go
@@ -11,11 +11,12 @@ import (
"testing"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/goversion"
"golang.org/x/tools/internal/gocommand"
+ "golang.org/x/tools/internal/testenv"
. "golang.org/x/tools/gopls/internal/test/integration"
)
@@ -248,6 +249,25 @@ func TestAutomaticWorkspaceModule_Interdependent(t *testing.T) {
})
}
+func TestWorkspaceVendoring(t *testing.T) {
+ testenv.NeedsGo1Point(t, 22)
+ WithOptions(
+ ProxyFiles(workspaceModuleProxy),
+ ).Run(t, multiModule, func(t *testing.T, env *Env) {
+ env.RunGoCommand("work", "init")
+ env.RunGoCommand("work", "use", "moda/a")
+ env.AfterChange()
+ env.OpenFile("moda/a/a.go")
+ env.RunGoCommand("work", "vendor")
+ env.AfterChange()
+ loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "b.(Hello)"))
+ const want = "vendor/b.com/b/b.go"
+ if got := env.Sandbox.Workdir.URIToPath(loc.URI); got != want {
+ t.Errorf("Definition: got location %q, want %q", got, want)
+ }
+ })
+}
+
func TestModuleWithExclude(t *testing.T) {
const proxy = `
-- c.com@v1.2.3/go.mod --
@@ -798,6 +818,51 @@ use (
})
}
+func TestInnerGoWork(t *testing.T) {
+ // This test checks that gopls honors a go.work file defined
+ // inside a go module (golang/go#63917).
+ const workspace = `
+-- go.mod --
+module a.com
+
+require b.com v1.2.3
+-- a/go.work --
+go 1.18
+
+use (
+ ..
+ ../b
+)
+-- a/a.go --
+package a
+
+import "b.com/b"
+
+var _ = b.B
+-- b/go.mod --
+module b.com/b
+
+-- b/b.go --
+package b
+
+const B = 0
+`
+ WithOptions(
+ // This doesn't work if we open the outer module. I'm not sure it should,
+ // since the go.work file does not apply to the entire module, just a
+ // subdirectory.
+ WorkspaceFolders("a"),
+ ).Run(t, workspace, func(t *testing.T, env *Env) {
+ env.OpenFile("a/a.go")
+ loc := env.GoToDefinition(env.RegexpSearch("a/a.go", "b.(B)"))
+ got := env.Sandbox.Workdir.URIToPath(loc.URI)
+ want := "b/b.go"
+ if got != want {
+ t.Errorf("Definition(b.B): got %q, want %q", got, want)
+ }
+ })
+}
+
func TestNonWorkspaceFileCreation(t *testing.T) {
const files = `
-- work/go.mod --
diff --git a/gopls/internal/test/integration/workspace/zero_config_test.go b/gopls/internal/test/integration/workspace/zero_config_test.go
index dd75c591ddb..93c24886c9e 100644
--- a/gopls/internal/test/integration/workspace/zero_config_test.go
+++ b/gopls/internal/test/integration/workspace/zero_config_test.go
@@ -8,8 +8,8 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/command"
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/protocol/command"
. "golang.org/x/tools/gopls/internal/test/integration"
)
@@ -185,3 +185,86 @@ const C = 0
)
})
}
+
+func TestGoModReplace(t *testing.T) {
+ // This test checks that we treat locally replaced modules as workspace
+ // modules, according to the "includeReplaceInWorkspace" setting.
+ const files = `
+-- moda/go.mod --
+module golang.org/a
+
+require golang.org/b v1.2.3
+
+replace golang.org/b => ../modb
+
+go 1.20
+
+-- moda/a.go --
+package a
+
+import "golang.org/b"
+
+const A = b.B
+
+-- modb/go.mod --
+module golang.org/b
+
+go 1.20
+
+-- modb/b.go --
+package b
+
+const B = 1
+`
+
+ for useReplace, expectation := range map[bool]Expectation{
+ true: FileWatchMatching("modb"),
+ false: NoFileWatchMatching("modb"),
+ } {
+ WithOptions(
+ WorkspaceFolders("moda"),
+ Settings{
+ "includeReplaceInWorkspace": useReplace,
+ },
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OnceMet(
+ InitialWorkspaceLoad,
+ expectation,
+ )
+ })
+ }
+}
+
+func TestDisableZeroConfig(t *testing.T) {
+ // This test checks that we treat locally replaced modules as workspace
+ // modules, according to the "includeReplaceInWorkspace" setting.
+ const files = `
+-- moda/go.mod --
+module golang.org/a
+
+go 1.20
+
+-- moda/a.go --
+package a
+
+-- modb/go.mod --
+module golang.org/b
+
+go 1.20
+
+-- modb/b.go --
+package b
+
+`
+
+ WithOptions(
+ Settings{"zeroConfig": false},
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("moda/a.go")
+ env.OpenFile("modb/b.go")
+ env.AfterChange()
+ if got := env.Views(); len(got) != 1 || got[0].Type != cache.AdHocView.String() {
+ t.Errorf("Views: got %v, want one adhoc view", got)
+ }
+ })
+}
diff --git a/gopls/internal/test/integration/wrappers.go b/gopls/internal/test/integration/wrappers.go
index ebe25b69ebe..cc4a66d79fd 100644
--- a/gopls/internal/test/integration/wrappers.go
+++ b/gopls/internal/test/integration/wrappers.go
@@ -8,8 +8,8 @@ import (
"encoding/json"
"path"
- "golang.org/x/tools/gopls/internal/lsp/command"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/protocol/command"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/xcontext"
)
diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go
index bf8b3634809..212f09c78a7 100644
--- a/gopls/internal/test/marker/doc.go
+++ b/gopls/internal/test/marker/doc.go
@@ -204,7 +204,7 @@ The following markers are supported within marker tests:
signatureHelp at the given location should match the provided string, with
the active parameter (an index) highlighted.
- - suggestedfix(location, regexp, kind, golden): like diag, the location and
+ - suggestedfix(location, regexp, golden): like diag, the location and
regexp identify an expected diagnostic. This diagnostic must
to have exactly one associated code action of the specified kind.
This action is executed for its editing effects on the source files.
diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go
index f5b068e3343..1652eec7282 100644
--- a/gopls/internal/test/marker/marker_test.go
+++ b/gopls/internal/test/marker/marker_test.go
@@ -30,11 +30,11 @@ import (
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/go/expect"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/debug"
"golang.org/x/tools/gopls/internal/hooks"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/lsprpc"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsprpc"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/test/compare"
"golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
@@ -85,8 +85,15 @@ func TestMain(m *testing.M) {
// - The old tests lacked documentation, and often had failures that were hard
// to understand. By starting from scratch, we can revisit these aspects.
func Test(t *testing.T) {
- if testing.Short() && strings.HasPrefix(os.Getenv("GO_BUILDER_NAME"), "darwin-") {
- t.Skip("golang/go#64473: skipping with -short: this test is too slow on darwin builders")
+ if testing.Short() {
+ builder := os.Getenv("GO_BUILDER_NAME")
+ // Note that HasPrefix(builder, "darwin-" only matches legacy builders.
+ // LUCI builder names start with x_tools-goN.NN.
+ // We want to exclude solaris on both legacy and LUCI builders, as
+ // it is timing out.
+ if strings.HasPrefix(builder, "darwin-") || strings.Contains(builder, "solaris") {
+ t.Skip("golang/go#64473: skipping with -short: this test is too slow on darwin and solaris builders")
+ }
}
// The marker tests must be able to run go/packages.Load.
testenv.NeedsGoPackages(t)
@@ -695,6 +702,15 @@ func loadMarkerTest(name string, content []byte) (*markerTest, error) {
return nil, fmt.Errorf("%s:%d: unwanted space before marker (// @)", file.Name, line)
}
+ // The 'go list' command doesn't work correct with modules named
+ // testdata", so don't allow it as a module name (golang/go#65406).
+ // (Otherwise files within it will end up in an ad hoc
+ // package, "command-line-arguments/$TMPDIR/...".)
+ if filepath.Base(file.Name) == "go.mod" &&
+ bytes.Contains(file.Data, []byte("module testdata")) {
+ return nil, fmt.Errorf("'testdata' is not a valid module name")
+ }
+
test.notes = append(test.notes, notes...)
test.files[file.Name] = file.Data
}
@@ -789,7 +805,7 @@ func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byt
// Put a debug instance in the context to prevent logging to stderr.
// See associated TODO in runner.go: we should revisit this pattern.
ctx := context.Background()
- ctx = debug.WithInstance(ctx, "", "off")
+ ctx = debug.WithInstance(ctx, "off")
awaiter := integration.NewAwaiter(sandbox.Workdir)
ss := lsprpc.NewStreamServer(cache, false, hooks.Options)
@@ -1916,6 +1932,23 @@ func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng proto
// applied in that order. But since applyDocumentChanges(env,
// action.Edit.DocumentChanges) doesn't compose, for now we
// assert that actions return one or the other.
+
+ // Resolve code action edits first if the client has resolve support
+ // and the code action has no edits.
+ if action.Edit == nil {
+ editSupport, err := env.Editor.EditResolveSupport()
+ if err != nil {
+ return nil, err
+ }
+ if editSupport {
+ resolved, err := env.Editor.Server.ResolveCodeAction(env.Ctx, &action)
+ if err != nil {
+ return nil, err
+ }
+ action.Edit = resolved.Edit
+ }
+ }
+
if action.Edit != nil {
if action.Edit.Changes != nil {
env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title)
diff --git a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt
new file mode 100644
index 00000000000..d5dbe931226
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt
@@ -0,0 +1,28 @@
+This test exercises extract on a variadic function.
+It is a regression test for bug #63287 in which
+the final paramater's "..." would go missing.
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- a/a.go --
+package a
+
+//@codeactionedit(block, "refactor.extract", out, "Extract function")
+
+func _() {
+ var logf func(string, ...any)
+ { println(logf) } //@loc(block, re`{.*}`)
+}
+
+-- @out/a/a.go --
+@@ -7 +7 @@
+- { println(logf) } //@loc(block, re`{.*}`)
++ { newFunction(logf) } //@loc(block, re`{.*}`)
+@@ -10 +10,4 @@
++func newFunction(logf func( string, ...any)) {
++ println(logf)
++}
++
+-- end --
diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt
index c10b8185c9b..685b4ff9372 100644
--- a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt
@@ -1,4 +1,5 @@
This test checks the behavior of the 'extract variable' code action.
+See extract_variable_resolve.txt for the same test with resolve support.
-- flags --
-ignore_extra_diags
diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt
new file mode 100644
index 00000000000..dc6ad787afb
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt
@@ -0,0 +1,81 @@
+This test checks the behavior of the 'extract variable' code action, with resolve support.
+See extract_variable.txt for the same test without resolve support.
+
+-- capabilities.json --
+{
+ "textDocument": {
+ "codeAction": {
+ "dataSupport": true,
+ "resolveSupport": {
+ "properties": ["edit"]
+ }
+ }
+ }
+}
+-- flags --
+-ignore_extra_diags
+
+-- basic_lit.go --
+package extract
+
+func _() {
+ var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1)
+ var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2)
+}
+
+-- @basic_lit1/basic_lit.go --
+@@ -4 +4,2 @@
+- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1)
++ x := 1
++ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1)
+-- @basic_lit2/basic_lit.go --
+@@ -5 +5,2 @@
+- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2)
++ x := 3 + 4
++ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2)
+-- func_call.go --
+package extract
+
+import "strconv"
+
+func _() {
+ x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1)
+ str := "1"
+ b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2)
+}
+
+-- @func_call1/func_call.go --
+@@ -6 +6,2 @@
+- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1)
++ x := append([]int{}, 1)
++ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1)
+-- @func_call2/func_call.go --
+@@ -8 +8,2 @@
+- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2)
++ x, x1 := strconv.Atoi(str)
++ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2)
+-- scope.go --
+package extract
+
+import "go/ast"
+
+func _() {
+ x0 := 0
+ if true {
+ y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1)
+ }
+ if true {
+ x1 := !false //@codeactionedit("!false", "refactor.extract", scope2)
+ }
+}
+
+-- @scope1/scope.go --
+@@ -8 +8,2 @@
+- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1)
++ x := ast.CompositeLit{}
++ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1)
+-- @scope2/scope.go --
+@@ -11 +11,2 @@
+- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2)
++ x := !false
++ x1 := x //@codeactionedit("!false", "refactor.extract", scope2)
diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt
index 092a4275ffb..deac1d78507 100644
--- a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt
@@ -1,4 +1,5 @@
This test checks the behavior of the 'fill struct' code action.
+See fill_struct_resolve.txt for same test with resolve support.
-- flags --
-ignore_extra_diags
@@ -89,11 +90,11 @@ type funStruct struct {
var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22)
-type funStructCompex struct {
+type funStructComplex struct {
fn func(i int, s string) (string, int)
}
-var _ = funStructCompex{} //@codeactionedit("}", "refactor.rewrite", a23)
+var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23)
type funStructEmpty struct {
fn func()
@@ -120,8 +121,8 @@ var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24)
+} //@codeactionedit("}", "refactor.rewrite", a22)
-- @a23/a2.go --
@@ -23 +23,4 @@
--var _ = funStructCompex{} //@codeactionedit("}", "refactor.rewrite", a23)
-+var _ = funStructCompex{
+-var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23)
++var _ = funStructComplex{
+ fn: func(i int, s string) (string, int) {
+ },
+} //@codeactionedit("}", "refactor.rewrite", a23)
diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt
new file mode 100644
index 00000000000..e553d1c5993
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt
@@ -0,0 +1,586 @@
+This test checks the behavior of the 'fill struct' code action, with resolve support.
+See fill_struct.txt for same test without resolve support.
+
+-- capabilities.json --
+{
+ "textDocument": {
+ "codeAction": {
+ "dataSupport": true,
+ "resolveSupport": {
+ "properties": ["edit"]
+ }
+ }
+ }
+}
+-- flags --
+-ignore_extra_diags
+
+-- go.mod --
+module golang.org/lsptests/fillstruct
+
+go 1.18
+
+-- data/data.go --
+package data
+
+type B struct {
+ ExportedInt int
+ unexportedInt int
+}
+
+-- a.go --
+package fillstruct
+
+import (
+ "golang.org/lsptests/fillstruct/data"
+)
+
+type basicStruct struct {
+ foo int
+}
+
+var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1)
+
+type twoArgStruct struct {
+ foo int
+ bar string
+}
+
+var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2)
+
+type nestedStruct struct {
+ bar string
+ basic basicStruct
+}
+
+var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3)
+
+var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4)
+-- @a1/a.go --
+@@ -11 +11,3 @@
+-var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1)
++var _ = basicStruct{
++ foo: 0,
++} //@codeactionedit("}", "refactor.rewrite", a1)
+-- @a2/a.go --
+@@ -18 +18,4 @@
+-var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2)
++var _ = twoArgStruct{
++ foo: 0,
++ bar: "",
++} //@codeactionedit("}", "refactor.rewrite", a2)
+-- @a3/a.go --
+@@ -25 +25,4 @@
+-var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3)
++var _ = nestedStruct{
++ bar: "",
++ basic: basicStruct{},
++} //@codeactionedit("}", "refactor.rewrite", a3)
+-- @a4/a.go --
+@@ -27 +27,3 @@
+-var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4)
++var _ = data.B{
++ ExportedInt: 0,
++} //@codeactionedit("}", "refactor.rewrite", a4)
+-- a2.go --
+package fillstruct
+
+type typedStruct struct {
+ m map[string]int
+ s []int
+ c chan int
+ c1 <-chan int
+ a [2]string
+}
+
+var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21)
+
+type funStruct struct {
+ fn func(i int) int
+}
+
+var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22)
+
+type funStructComplex struct {
+ fn func(i int, s string) (string, int)
+}
+
+var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23)
+
+type funStructEmpty struct {
+ fn func()
+}
+
+var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24)
+
+-- @a21/a2.go --
+@@ -11 +11,7 @@
+-var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21)
++var _ = typedStruct{
++ m: map[string]int{},
++ s: []int{},
++ c: make(chan int),
++ c1: make(<-chan int),
++ a: [2]string{},
++} //@codeactionedit("}", "refactor.rewrite", a21)
+-- @a22/a2.go --
+@@ -17 +17,4 @@
+-var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22)
++var _ = funStruct{
++ fn: func(i int) int {
++ },
++} //@codeactionedit("}", "refactor.rewrite", a22)
+-- @a23/a2.go --
+@@ -23 +23,4 @@
+-var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23)
++var _ = funStructComplex{
++ fn: func(i int, s string) (string, int) {
++ },
++} //@codeactionedit("}", "refactor.rewrite", a23)
+-- @a24/a2.go --
+@@ -29 +29,4 @@
+-var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24)
++var _ = funStructEmpty{
++ fn: func() {
++ },
++} //@codeactionedit("}", "refactor.rewrite", a24)
+-- a3.go --
+package fillstruct
+
+import (
+ "go/ast"
+ "go/token"
+)
+
+type Foo struct {
+ A int
+}
+
+type Bar struct {
+ X *Foo
+ Y *Foo
+}
+
+var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31)
+
+type importedStruct struct {
+ m map[*ast.CompositeLit]ast.Field
+ s []ast.BadExpr
+ a [3]token.Token
+ c chan ast.EmptyStmt
+ fn func(ast_decl ast.DeclStmt) ast.Ellipsis
+ st ast.CompositeLit
+}
+
+var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32)
+
+type pointerBuiltinStruct struct {
+ b *bool
+ s *string
+ i *int
+}
+
+var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33)
+
+var _ = []ast.BasicLit{
+ {}, //@codeactionedit("}", "refactor.rewrite", a34)
+}
+
+var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35)
+-- @a31/a3.go --
+@@ -17 +17,4 @@
+-var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31)
++var _ = Bar{
++ X: &Foo{},
++ Y: &Foo{},
++} //@codeactionedit("}", "refactor.rewrite", a31)
+-- @a32/a3.go --
+@@ -28 +28,9 @@
+-var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32)
++var _ = importedStruct{
++ m: map[*ast.CompositeLit]ast.Field{},
++ s: []ast.BadExpr{},
++ a: [3]token.Token{},
++ c: make(chan ast.EmptyStmt),
++ fn: func(ast_decl ast.DeclStmt) ast.Ellipsis {
++ },
++ st: ast.CompositeLit{},
++} //@codeactionedit("}", "refactor.rewrite", a32)
+-- @a33/a3.go --
+@@ -36 +36,5 @@
+-var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33)
++var _ = pointerBuiltinStruct{
++ b: new(bool),
++ s: new(string),
++ i: new(int),
++} //@codeactionedit("}", "refactor.rewrite", a33)
+-- @a34/a3.go --
+@@ -39 +39,5 @@
+- {}, //@codeactionedit("}", "refactor.rewrite", a34)
++ {
++ ValuePos: 0,
++ Kind: 0,
++ Value: "",
++ }, //@codeactionedit("}", "refactor.rewrite", a34)
+-- @a35/a3.go --
+@@ -42 +42,5 @@
+-var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35)
++var _ = []ast.BasicLit{{
++ ValuePos: 0,
++ Kind: 0,
++ Value: "",
++}} //@codeactionedit("}", "refactor.rewrite", a35)
+-- a4.go --
+package fillstruct
+
+import "go/ast"
+
+type iStruct struct {
+ X int
+}
+
+type sStruct struct {
+ str string
+}
+
+type multiFill struct {
+ num int
+ strin string
+ arr []int
+}
+
+type assignStruct struct {
+ n ast.Node
+}
+
+func fill() {
+ var x int
+ var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41)
+
+ var s string
+ var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42)
+
+ var n int
+ _ = []int{}
+ if true {
+ arr := []int{1, 2}
+ }
+ var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43)
+
+ var node *ast.CompositeLit
+ var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45)
+}
+
+-- @a41/a4.go --
+@@ -25 +25,3 @@
+- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41)
++ var _ = iStruct{
++ X: x,
++ } //@codeactionedit("}", "refactor.rewrite", a41)
+-- @a42/a4.go --
+@@ -28 +28,3 @@
+- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42)
++ var _ = sStruct{
++ str: s,
++ } //@codeactionedit("}", "refactor.rewrite", a42)
+-- @a43/a4.go --
+@@ -35 +35,5 @@
+- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43)
++ var _ = multiFill{
++ num: n,
++ strin: s,
++ arr: []int{},
++ } //@codeactionedit("}", "refactor.rewrite", a43)
+-- @a45/a4.go --
+@@ -38 +38,3 @@
+- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45)
++ var _ = assignStruct{
++ n: node,
++ } //@codeactionedit("}", "refactor.rewrite", a45)
+-- fill_struct.go --
+package fillstruct
+
+type StructA struct {
+ unexportedIntField int
+ ExportedIntField int
+ MapA map[int]string
+ Array []int
+ StructB
+}
+
+type StructA2 struct {
+ B *StructB
+}
+
+type StructA3 struct {
+ B StructB
+}
+
+func fill() {
+ a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1)
+ b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2)
+ c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3)
+ if true {
+ _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4)
+ }
+}
+
+-- @fill_struct1/fill_struct.go --
+@@ -20 +20,7 @@
+- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1)
++ a := StructA{
++ unexportedIntField: 0,
++ ExportedIntField: 0,
++ MapA: map[int]string{},
++ Array: []int{},
++ StructB: StructB{},
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct1)
+-- @fill_struct2/fill_struct.go --
+@@ -21 +21,3 @@
+- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2)
++ b := StructA2{
++ B: &StructB{},
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct2)
+-- @fill_struct3/fill_struct.go --
+@@ -22 +22,3 @@
+- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3)
++ c := StructA3{
++ B: StructB{},
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct3)
+-- @fill_struct4/fill_struct.go --
+@@ -24 +24,3 @@
+- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4)
++ _ = StructA3{
++ B: StructB{},
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct4)
+-- fill_struct_anon.go --
+package fillstruct
+
+type StructAnon struct {
+ a struct{}
+ b map[string]interface{}
+ c map[string]struct {
+ d int
+ e bool
+ }
+}
+
+func fill() {
+ _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon)
+}
+-- @fill_struct_anon/fill_struct_anon.go --
+@@ -13 +13,5 @@
+- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon)
++ _ := StructAnon{
++ a: struct{}{},
++ b: map[string]interface{}{},
++ c: map[string]struct{d int; e bool}{},
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon)
+-- fill_struct_nested.go --
+package fillstruct
+
+type StructB struct {
+ StructC
+}
+
+type StructC struct {
+ unexportedInt int
+}
+
+func nested() {
+ c := StructB{
+ StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested)
+ }
+}
+
+-- @fill_nested/fill_struct_nested.go --
+@@ -13 +13,3 @@
+- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested)
++ StructC: StructC{
++ unexportedInt: 0,
++ }, //@codeactionedit("}", "refactor.rewrite", fill_nested)
+-- fill_struct_package.go --
+package fillstruct
+
+import (
+ h2 "net/http"
+
+ "golang.org/lsptests/fillstruct/data"
+)
+
+func unexported() {
+ a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1)
+ _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2)
+}
+-- @fill_struct_package1/fill_struct_package.go --
+@@ -10 +10,3 @@
+- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1)
++ a := data.B{
++ ExportedInt: 0,
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1)
+-- @fill_struct_package2/fill_struct_package.go --
+@@ -11 +11,7 @@
+- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2)
++ _ = h2.Client{
++ Transport: nil,
++ CheckRedirect: func(req *h2.Request, via []*h2.Request) error {
++ },
++ Jar: nil,
++ Timeout: 0,
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2)
+-- fill_struct_partial.go --
+package fillstruct
+
+type StructPartialA struct {
+ PrefilledInt int
+ UnfilledInt int
+ StructPartialB
+}
+
+type StructPartialB struct {
+ PrefilledInt int
+ UnfilledInt int
+}
+
+func fill() {
+ a := StructPartialA{
+ PrefilledInt: 5,
+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1)
+ b := StructPartialB{
+ /* this comment should disappear */
+ PrefilledInt: 7, // This comment should be blown away.
+ /* As should
+ this one */
+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2)
+}
+
+-- @fill_struct_partial1/fill_struct_partial.go --
+@@ -16 +16,3 @@
+- PrefilledInt: 5,
++ PrefilledInt: 5,
++ UnfilledInt: 0,
++ StructPartialB: StructPartialB{},
+-- @fill_struct_partial2/fill_struct_partial.go --
+@@ -19,4 +19,2 @@
+- /* this comment should disappear */
+- PrefilledInt: 7, // This comment should be blown away.
+- /* As should
+- this one */
++ PrefilledInt: 7,
++ UnfilledInt: 0,
+-- fill_struct_spaces.go --
+package fillstruct
+
+type StructD struct {
+ ExportedIntField int
+}
+
+func spaces() {
+ d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces)
+}
+
+-- @fill_struct_spaces/fill_struct_spaces.go --
+@@ -8 +8,3 @@
+- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces)
++ d := StructD{
++ ExportedIntField: 0,
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces)
+-- fill_struct_unsafe.go --
+package fillstruct
+
+import "unsafe"
+
+type unsafeStruct struct {
+ x int
+ p unsafe.Pointer
+}
+
+func fill() {
+ _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe)
+}
+
+-- @fill_struct_unsafe/fill_struct_unsafe.go --
+@@ -11 +11,4 @@
+- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe)
++ _ := unsafeStruct{
++ x: 0,
++ p: nil,
++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe)
+-- typeparams.go --
+package fillstruct
+
+type emptyStructWithTypeParams[A any] struct{}
+
+var _ = emptyStructWithTypeParams[int]{} // no suggested fix
+
+type basicStructWithTypeParams[T any] struct {
+ foo T
+}
+
+var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1)
+
+type twoArgStructWithTypeParams[F, B any] struct {
+ foo F
+ bar B
+}
+
+var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2)
+
+var _ = twoArgStructWithTypeParams[int, string]{
+ bar: "bar",
+} //@codeactionedit("}", "refactor.rewrite", typeparams3)
+
+type nestedStructWithTypeParams struct {
+ bar string
+ basic basicStructWithTypeParams[int]
+}
+
+var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4)
+
+func _[T any]() {
+ type S struct{ t T }
+ _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5)
+}
+-- @typeparams1/typeparams.go --
+@@ -11 +11,3 @@
+-var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1)
++var _ = basicStructWithTypeParams[int]{
++ foo: 0,
++} //@codeactionedit("}", "refactor.rewrite", typeparams1)
+-- @typeparams2/typeparams.go --
+@@ -18 +18,4 @@
+-var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2)
++var _ = twoArgStructWithTypeParams[string, int]{
++ foo: "",
++ bar: 0,
++} //@codeactionedit("}", "refactor.rewrite", typeparams2)
+-- @typeparams3/typeparams.go --
+@@ -21 +21 @@
++ foo: 0,
+-- @typeparams4/typeparams.go --
+@@ -29 +29,4 @@
+-var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4)
++var _ = nestedStructWithTypeParams{
++ bar: "",
++ basic: basicStructWithTypeParams{},
++} //@codeactionedit("}", "refactor.rewrite", typeparams4)
+-- @typeparams5/typeparams.go --
+@@ -33 +33,3 @@
+- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5)
++ _ = S{
++ t: *new(T),
++ } //@codeactionedit("}", "refactor.rewrite", typeparams5)
+-- issue63921.go --
+package fillstruct
+
+// Test for golang/go#63921: fillstruct panicked with invalid fields.
+type invalidStruct struct {
+ F int
+ Undefined
+}
+
+func _() {
+ // Note: the golden content for issue63921 is empty: fillstruct produces no
+ // edits, but does not panic.
+ invalidStruct{} //@codeactionedit("}", "refactor.rewrite", issue63921)
+}
diff --git a/gopls/internal/test/marker/testdata/codeaction/import-shadows-builtin.txt b/gopls/internal/test/marker/testdata/codeaction/import-shadows-builtin.txt
new file mode 100644
index 00000000000..aeb86a22686
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/import-shadows-builtin.txt
@@ -0,0 +1,55 @@
+This is a regression test for bug #63592 in "organize imports" whereby
+the new imports would shadow predeclared names.
+
+In the original example, the conflict was between predeclared error
+type and the unfortunately named package github.com/coreos/etcd/error,
+but this example uses a package with the ludicrous name of complex128.
+
+The new behavior is that we will not attempt to import packages
+that shadow predeclared names. (Ideally we would do that only if
+the predeclared name is actually referenced in the file, which
+complex128 happens to be in this example, but that's a trickier
+analysis than the internal/imports package is game for.)
+
+The name complex127 works as usual.
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- complex128/a.go --
+package complex128
+
+var V int
+
+-- complex127/a.go --
+package complex127
+
+var V int
+
+-- main.go --
+package main
+
+import () //@codeaction("import", "", "source.organizeImports", out)
+
+func main() {
+ complex128.V() //@diag("V", re"type complex128 has no field")
+ complex127.V() //@diag("complex127", re"(undeclared|undefined)")
+}
+
+func _() {
+ var _ complex128 = 1 + 2i
+}
+-- @out/main.go --
+package main
+
+import "example.com/complex127" //@codeaction("import", "", "source.organizeImports", out)
+
+func main() {
+ complex128.V() //@diag("V", re"type complex128 has no field")
+ complex127.V() //@diag("complex127", re"(undeclared|undefined)")
+}
+
+func _() {
+ var _ complex128 = 1 + 2i
+}
diff --git a/gopls/internal/test/marker/testdata/codeaction/infertypeargs.txt b/gopls/internal/test/marker/testdata/codeaction/infertypeargs.txt
index aa42e879467..b622efdc358 100644
--- a/gopls/internal/test/marker/testdata/codeaction/infertypeargs.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/infertypeargs.txt
@@ -15,21 +15,11 @@ func app[S interface{ ~[]E }, E interface{}](s S, e E) S {
func _() {
_ = app[[]int]
_ = app[[]int, int]
- _ = app[[]int]([]int{}, 0) //@codeaction("app", ")", "refactor.rewrite", infer)
+ _ = app[[]int]([]int{}, 0) //@suggestedfix("[[]int]", re"unnecessary type arguments", infer)
_ = app([]int{}, 0)
}
-- @infer/p.go --
-package infertypeargs
-
-func app[S interface{ ~[]E }, E interface{}](s S, e E) S {
- return append(s, e)
-}
-
-func _() {
- _ = app[[]int]
- _ = app[[]int, int]
- _ = app([]int{}, 0) //@codeaction("app", ")", "refactor.rewrite", infer)
- _ = app([]int{}, 0)
-}
-
+@@ -10 +10 @@
+- _ = app[[]int]([]int{}, 0) //@suggestedfix("[[]int]", re"unnecessary type arguments", infer)
++ _ = app([]int{}, 0) //@suggestedfix("[[]int]", re"unnecessary type arguments", infer)
diff --git a/gopls/internal/test/marker/testdata/codeaction/inline.txt b/gopls/internal/test/marker/testdata/codeaction/inline.txt
index 813a69ce09c..0c5bcb41658 100644
--- a/gopls/internal/test/marker/testdata/codeaction/inline.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/inline.txt
@@ -1,7 +1,8 @@
-This is a minimal test of the refactor.inline code action.
+This is a minimal test of the refactor.inline code action, without resolve support.
+See inline_resolve.txt for same test with resolve support.
-- go.mod --
-module testdata/codeaction
+module example.com/codeaction
go 1.18
-- a/a.go --
diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt
new file mode 100644
index 00000000000..02c27e6505b
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt
@@ -0,0 +1,35 @@
+This is a minimal test of the refactor.inline code actions, with resolve support.
+See inline.txt for same test without resolve support.
+
+-- capabilities.json --
+{
+ "textDocument": {
+ "codeAction": {
+ "dataSupport": true,
+ "resolveSupport": {
+ "properties": ["edit"]
+ }
+ }
+ }
+}
+-- go.mod --
+module example.com/codeaction
+go 1.18
+
+-- a/a.go --
+package a
+
+func _() {
+ println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline)
+}
+
+func add(x, y int) int { return x + y }
+
+-- @inline/a/a.go --
+package a
+
+func _() {
+ println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline)
+}
+
+func add(x, y int) int { return x + y }
diff --git a/gopls/internal/test/marker/testdata/codeaction/issue64558.txt b/gopls/internal/test/marker/testdata/codeaction/issue64558.txt
index 71d966159f8..59aaffba371 100644
--- a/gopls/internal/test/marker/testdata/codeaction/issue64558.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/issue64558.txt
@@ -1,7 +1,7 @@
Test of an inlining failure due to an ill-typed input program (#64558).
-- go.mod --
-module testdata
+module example.com
go 1.18
-- a/a.go --
diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt
index ad2289284d8..17a058f2b82 100644
--- a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt
@@ -1,4 +1,5 @@
This test exercises the refactoring to remove unused parameters.
+See removeparam_resolve.txt for same test with resolve support.
-- go.mod --
module unused.mod
@@ -202,7 +203,7 @@ func _() {
-- effects/effects.go --
package effects
-func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite", effects)
+func effects(x, y int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite", effects)
return x
}
@@ -216,7 +217,7 @@ func _() {
-- @effects/effects/effects.go --
package effects
-func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite", effects)
+func effects(x int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite", effects)
return x
}
diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt
new file mode 100644
index 00000000000..1c04aa047cf
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt
@@ -0,0 +1,258 @@
+This test exercises the refactoring to remove unused parameters, with resolve support.
+See removeparam.txt for same test without resolve support.
+
+-- capabilities.json --
+{
+ "textDocument": {
+ "codeAction": {
+ "dataSupport": true,
+ "resolveSupport": {
+ "properties": ["edit"]
+ }
+ }
+ }
+}
+-- go.mod --
+module unused.mod
+
+go 1.18
+
+-- a/a.go --
+package a
+
+func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a)
+ return x
+}
+
+-- @a/a/a.go --
+package a
+
+func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a)
+ return x
+}
+
+-- a/a2.go --
+package a
+
+func _() {
+ A(1, 2)
+}
+
+-- a/a_test.go --
+package a
+
+func _() {
+ A(1, 2)
+}
+
+-- a/a_x_test.go --
+package a_test
+
+import "unused.mod/a"
+
+func _() {
+ a.A(1, 2)
+}
+
+-- b/b.go --
+package b
+
+import "unused.mod/a"
+
+func f() int {
+ return 1
+}
+
+func g() int {
+ return 2
+}
+
+func _() {
+ a.A(f(), 1)
+}
+
+-- @a/a/a2.go --
+package a
+
+func _() {
+ A(1)
+}
+-- @a/a/a_test.go --
+package a
+
+func _() {
+ A(1)
+}
+-- @a/a/a_x_test.go --
+package a_test
+
+import "unused.mod/a"
+
+func _() {
+ a.A(1)
+}
+-- @a/b/b.go --
+package b
+
+import "unused.mod/a"
+
+func f() int {
+ return 1
+}
+
+func g() int {
+ return 2
+}
+
+func _() {
+ a.A(f())
+}
+-- field/field.go --
+package field
+
+func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field)
+}
+
+func _() {
+ Field(1, 2)
+}
+-- @field/field/field.go --
+package field
+
+func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field)
+}
+
+func _() {
+ Field(2)
+}
+-- ellipsis/ellipsis.go --
+package ellipsis
+
+func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis)
+}
+
+func _() {
+ // TODO(rfindley): investigate the broken formatting resulting from these inlinings.
+ Ellipsis()
+ Ellipsis(1)
+ Ellipsis(1, 2)
+ Ellipsis(1, f(), g())
+ Ellipsis(h())
+ Ellipsis(i()...)
+}
+
+func f() int
+func g() int
+func h() (int, int)
+func i() []any
+
+-- @ellipsis/ellipsis/ellipsis.go --
+package ellipsis
+
+func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis)
+}
+
+func _() {
+ // TODO(rfindley): investigate the broken formatting resulting from these inlinings.
+ Ellipsis()
+ Ellipsis()
+ Ellipsis()
+ var _ []any = []any{1, f(), g()}
+ Ellipsis()
+ func(_ ...any) {
+ Ellipsis()
+ }(h())
+ var _ []any = i()
+ Ellipsis()
+}
+
+func f() int
+func g() int
+func h() (int, int)
+func i() []any
+-- ellipsis2/ellipsis2.go --
+package ellipsis2
+
+func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2)
+}
+
+func _() {
+ Ellipsis2(1,2,3)
+ Ellipsis2(h())
+ Ellipsis2(1,2, []int{3, 4}...)
+}
+
+func h() (int, int)
+
+-- @ellipsis2/ellipsis2/ellipsis2.go --
+package ellipsis2
+
+func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2)
+}
+
+func _() {
+ Ellipsis2(2, []int{3}...)
+ func(_, blank0 int, rest ...int) {
+ Ellipsis2(blank0, rest...)
+ }(h())
+ Ellipsis2(2, []int{3, 4}...)
+}
+
+func h() (int, int)
+-- overlapping/overlapping.go --
+package overlapping
+
+func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping")
+ return 0
+}
+
+func _() {
+ x := Overlapping(Overlapping(0))
+ _ = x
+}
+
+-- effects/effects.go --
+package effects
+
+func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite", effects), diag("y", re"unused")
+ return x
+}
+
+func f() int
+func g() int
+
+func _() {
+ effects(f(), g())
+ effects(f(), g())
+}
+-- @effects/effects/effects.go --
+package effects
+
+func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite", effects), diag("y", re"unused")
+ return x
+}
+
+func f() int
+func g() int
+
+func _() {
+ var x, _ int = f(), g()
+ effects(x)
+ {
+ var x, _ int = f(), g()
+ effects(x)
+ }
+}
+-- recursive/recursive.go --
+package recursive
+
+func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive)
+ return Recursive(1)
+}
+
+-- @recursive/recursive/recursive.go --
+package recursive
+
+func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive)
+ return Recursive()
+}
diff --git a/gopls/internal/test/marker/testdata/codelens/test.txt b/gopls/internal/test/marker/testdata/codelens/test.txt
index 90782bddef9..ba68cf019df 100644
--- a/gopls/internal/test/marker/testdata/codelens/test.txt
+++ b/gopls/internal/test/marker/testdata/codelens/test.txt
@@ -22,7 +22,8 @@ func TestMain(m *testing.M) {} // no code lens for TestMain
func TestFuncWithCodeLens(t *testing.T) { //@codelens(re"()func", "run test")
}
-func thisShouldNotHaveACodeLens(t *testing.T) {
+func thisShouldNotHaveACodeLens(t *testing.T) { //@diag("t ", re"unused parameter")
+ println() // nonempty body => "unused parameter"
}
func BenchmarkFuncWithCodeLens(b *testing.B) { //@codelens(re"()func", "run benchmark")
diff --git a/gopls/internal/test/marker/testdata/completion/func_snippets.txt b/gopls/internal/test/marker/testdata/completion/func_snippets.txt
index efbc393f30f..01316342b7f 100644
--- a/gopls/internal/test/marker/testdata/completion/func_snippets.txt
+++ b/gopls/internal/test/marker/testdata/completion/func_snippets.txt
@@ -28,5 +28,5 @@ func Identity[P ~int](p P) P { //@item(Identity, "Identity", "", "")
func _() {
_ = NewSyncM //@snippet(" //", NewSyncMap, "NewSyncMap[${1:K comparable}, ${2:V any}]()")
- _ = Identi //@snippet(" //", Identity, "Identity[${1:P ~int}](${2:p P})")
+ _ = Identi //@snippet(" //", Identity, "Identity(${1:p P})")
}
diff --git a/gopls/internal/test/marker/testdata/completion/issue51783.txt b/gopls/internal/test/marker/testdata/completion/issue51783.txt
new file mode 100644
index 00000000000..074259ca713
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/completion/issue51783.txt
@@ -0,0 +1,47 @@
+Regression test for "completion gives unneeded generic type
+instantiation snippet", #51783.
+
+Type parameters that can be inferred from the arguments
+are not part of the offered completion snippet.
+
+-- flags --
+-ignore_extra_diags
+
+-- a.go --
+package a
+
+// identity has a single simple type parameter.
+// The completion omits the instantiation.
+func identity[T any](x T) T
+
+// clone has a second type parameter that is nonetheless constrained by the parameter.
+// The completion omits the instantiation.
+func clone[S ~[]E, E any](s S) S
+
+// unconstrained has a type parameter constrained only by the result.
+// The completion suggests instantiation.
+func unconstrained[X, Y any](x X) Y
+
+// partial has three type parameters,
+// only the last two of which may be omitted as they
+// are constrained by the arguments.
+func partial[R any, S ~[]E, E any](s S) R
+
+//@item(identity, "identity", "details", "kind")
+//@item(clone, "clone", "details", "kind")
+//@item(unconstrained, "unconstrained", "details", "kind")
+//@item(partial, "partial", "details", "kind")
+
+func _() {
+ _ = identity //@snippet("identity", identity, "identity(${1:})")
+
+ _ = clone //@snippet("clone", clone, "clone(${1:})")
+
+ _ = unconstrained //@snippet("unconstrained", unconstrained, "unconstrained[${1:}](${2:})")
+
+ _ = partial //@snippet("partial", partial, "partial[${1:}](${2:})")
+
+ // Result-type inference permits us to omit Y in this (rare) case,
+ // but completion doesn't support that.
+ var _ int = unconstrained //@snippet("unconstrained", unconstrained, "unconstrained[${1:}](${2:})")
+}
diff --git a/gopls/internal/test/marker/testdata/completion/nested_complit.txt b/gopls/internal/test/marker/testdata/completion/nested_complit.txt
index f3a148dedf8..264ae77eab8 100644
--- a/gopls/internal/test/marker/testdata/completion/nested_complit.txt
+++ b/gopls/internal/test/marker/testdata/completion/nested_complit.txt
@@ -1,9 +1,11 @@
This test checks completion of nested composite literals;
-TODO(rfindley): investigate an un-skip the disabled test below.
+Parser recovery changed in Go 1.20, so this test requires at least that
+version for consistency.
-- flags --
-ignore_extra_diags
+-min_go=go1.20
-- nested_complit.go --
package nested_complit
@@ -15,9 +17,10 @@ type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct")
}
func _() {
- []ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var")
+ _ = []ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var")
+ _ = make([]ncFoo, 0) //@item(makeNCFoo, "make([]ncFoo, 0)", "", "func")
+
_ := ncBar{
- // disabled - see issue #54822
- baz: [] // complete(" //", structNCFoo, structNCBar)
+ baz: [] //@complete(" //", litNCFoo, makeNCFoo, structNCBar, structNCFoo)
}
}
diff --git a/gopls/internal/test/marker/testdata/completion/type_params.txt b/gopls/internal/test/marker/testdata/completion/type_params.txt
index 185d77f9911..8e2f5d7e401 100644
--- a/gopls/internal/test/marker/testdata/completion/type_params.txt
+++ b/gopls/internal/test/marker/testdata/completion/type_params.txt
@@ -52,8 +52,7 @@ func returnTP[A int | float64](a A) A { //@item(returnTP, "returnTP", "something
}
func _() {
- // disabled - see issue #54822
- var _ int = returnTP // snippet(" //", returnTP, "returnTP[${1:}](${2:})")
+ var _ int = returnTP //@snippet(" //", returnTP, "returnTP(${1:})")
var aa int //@item(tpInt, "aa", "int", "var")
var ab float64 //@item(tpFloat, "ab", "float64", "var")
diff --git a/gopls/internal/test/marker/testdata/configuration/static.txt b/gopls/internal/test/marker/testdata/configuration/static.txt
new file mode 100644
index 00000000000..c84b55db117
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/configuration/static.txt
@@ -0,0 +1,41 @@
+This test confirms that gopls honors configuration even if the client does not
+support dynamic configuration.
+
+-- capabilities.json --
+{
+ "configuration": false
+}
+
+-- settings.json --
+{
+ "usePlaceholders": true,
+ "analyses": {
+ "composites": false
+ }
+}
+
+-- go.mod --
+module example.com/config
+
+go 1.18
+
+-- a/a.go --
+package a
+
+import "example.com/config/b"
+
+func Identity[P ~int](p P) P { //@item(Identity, "Identity", "", "")
+ return p
+}
+
+func _() {
+ _ = b.B{2}
+ _ = Identi //@snippet(" //", Identity, "Identity(${1:p P})"), diag("Ident", re"(undefined|undeclared)")
+}
+
+-- b/b.go --
+package b
+
+type B struct {
+ F int
+}
diff --git a/gopls/internal/test/marker/testdata/definition/embed.txt b/gopls/internal/test/marker/testdata/definition/embed.txt
index 3212c147e3e..9842c37d047 100644
--- a/gopls/internal/test/marker/testdata/definition/embed.txt
+++ b/gopls/internal/test/marker/testdata/definition/embed.txt
@@ -182,6 +182,11 @@ type S1 struct {
}
```
+```go
+// Embedded fields:
+F2 int // through S2
+```
+
[`b.S1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1)
-- @S1F1 --
```go
@@ -210,6 +215,10 @@ type S2 struct {
}
```
+```go
+func (a.A) Hi()
+```
+
[`b.S2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2)
-- @S2F1 --
```go
@@ -241,19 +250,24 @@ field Field int
-- @aA --
```go
type A string
-
-func (a.A).Hi()
```
@loc(AString, "A")
+```go
+func (a.A) Hi()
+```
+
[`a.A` on pkg.go.dev](https://pkg.go.dev/mod.com/a#A)
-- @aAlias --
```go
type aAlias = a.A
-
-func (a.A).Hi()
```
@loc(aAlias, "aAlias")
+
+
+```go
+func (a.A) Hi()
+```
diff --git a/gopls/internal/test/marker/testdata/definition/misc.txt b/gopls/internal/test/marker/testdata/definition/misc.txt
index 2ab637a3ed4..c7147a625be 100644
--- a/gopls/internal/test/marker/testdata/definition/misc.txt
+++ b/gopls/internal/test/marker/testdata/definition/misc.txt
@@ -177,6 +177,10 @@ type I interface {
}
```
+```go
+func (J) Hello()
+```
+
[`a.I` on pkg.go.dev](https://pkg.go.dev/mod.com#I)
-- @hoverJ --
```go
diff --git a/gopls/internal/test/marker/testdata/definition/standalone.txt b/gopls/internal/test/marker/testdata/definition/standalone.txt
new file mode 100644
index 00000000000..6af1149184d
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/definition/standalone.txt
@@ -0,0 +1,42 @@
+This test checks the behavior of standalone packages, in particular documenting
+our failure to support test files as standalone packages (golang/go#64233).
+
+-- go.mod --
+module golang.org/lsptests/a
+
+go 1.20
+
+-- a.go --
+package a
+
+func F() {} //@loc(F, "F")
+
+-- standalone.go --
+//go:build ignore
+package main
+
+import "golang.org/lsptests/a"
+
+func main() {
+ a.F() //@def("F", F)
+}
+
+-- standalone_test.go --
+//go:build ignore
+package main //@diag("main", re"No packages found")
+
+import "golang.org/lsptests/a"
+
+func main() {
+ a.F() //@hovererr("F", "no package")
+}
+
+-- standalone_x_test.go --
+//go:build ignore
+package main_test //@diag("main", re"No packages found")
+
+import "golang.org/lsptests/a"
+
+func main() {
+ a.F() //@hovererr("F", "no package")
+}
diff --git a/gopls/internal/test/marker/testdata/diagnostics/addgowork.txt b/gopls/internal/test/marker/testdata/diagnostics/addgowork.txt
index 729547af412..5fbd890e65f 100644
--- a/gopls/internal/test/marker/testdata/diagnostics/addgowork.txt
+++ b/gopls/internal/test/marker/testdata/diagnostics/addgowork.txt
@@ -5,9 +5,13 @@ Quick-fixes change files on disk, so are tested by integration tests.
TODO(rfindley): improve the "cannot find package" import errors.
-- skip --
-Skipping due to go.dev/issue/60584#issuecomment-1622238115.
-There appears to be a real race in the critical error logic causing this test
-to flake with high frequency.
+These diagnostics are no longer produced, because in golang/go#57979
+(zero-config gopls) we made gopls function independent of a go.work file.
+Preserving this test as we may want to re-enable the code actions go manage
+a go.work file.
+
+Note that in go.dev/issue/60584#issuecomment-1622238115, this test was flaky.
+However, critical error logic has since been rewritten.
-- a/go.mod --
module mod.com/a
diff --git a/gopls/internal/test/marker/testdata/diagnostics/issue64547.txt b/gopls/internal/test/marker/testdata/diagnostics/issue64547.txt
new file mode 100644
index 00000000000..3f3e13bdf67
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/diagnostics/issue64547.txt
@@ -0,0 +1,14 @@
+This test checks the fix for golang/go#64547: the lostcancel analyzer reports
+diagnostics that overflow the file.
+
+-- p.go --
+package p
+
+import "context"
+
+func _() {
+ _, cancel := context.WithCancel(context.Background()) //@diag("_, cancel", re"not used on all paths")
+ if false {
+ cancel()
+ }
+} //@diag("}", re"may be reached without using the cancel")
diff --git a/gopls/internal/test/marker/testdata/format/issue59554.txt b/gopls/internal/test/marker/testdata/format/issue59554.txt
index aa6c2aa02a4..816c9d1e06f 100644
--- a/gopls/internal/test/marker/testdata/format/issue59554.txt
+++ b/gopls/internal/test/marker/testdata/format/issue59554.txt
@@ -4,7 +4,10 @@ directives.
Note that gofumpt is needed for this test case, as it reformats var decls into
short var decls.
-Note that gofumpt requires Go 1.18.
+Note that gofumpt requires Go 1.20.
+
+-- flags --
+-min_go=go1.20
-- settings.json --
{
diff --git a/gopls/internal/test/marker/testdata/highlight/issue60589.txt b/gopls/internal/test/marker/testdata/highlight/issue60589.txt
new file mode 100644
index 00000000000..afa4335a9c6
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/highlight/issue60589.txt
@@ -0,0 +1,30 @@
+This test verifies that control flow lighlighting correctly accounts for
+multi-name result parameters. In golang/go#60589, it did not.
+
+-- go.mod --
+module mod.com
+
+go 1.18
+
+-- p.go --
+package p
+
+func _() (foo int, bar, baz string) { //@ loc(func, "func"), loc(foo, "foo"), loc(fooint, "foo int"), loc(int, "int"), loc(bar, "bar"), loc(beforebaz, " baz"), loc(baz, "baz"), loc(barbazstring, "bar, baz string"), loc(beforestring, re`() string`), loc(string, "string")
+ return 0, "1", "2" //@ loc(return, `return 0, "1", "2"`), loc(l0, "0"), loc(l1, `"1"`), loc(l2, `"2"`)
+}
+
+// Assertions, expressed here to avoid clutter above.
+// Note that when the cursor is over the field type, there is some
+// (likely harmless) redundancy.
+
+//@ highlight(func, func, return)
+//@ highlight(foo, foo, l0)
+//@ highlight(int, fooint, int, l0)
+//@ highlight(bar, bar, l1)
+//@ highlight(beforebaz)
+//@ highlight(baz, baz, l2)
+//@ highlight(beforestring, baz, l2)
+//@ highlight(string, barbazstring, string, l1, l2)
+//@ highlight(l0, foo, l0)
+//@ highlight(l1, bar, l1)
+//@ highlight(l2, baz, l2)
diff --git a/gopls/internal/test/marker/testdata/highlight/issue65516.txt b/gopls/internal/test/marker/testdata/highlight/issue65516.txt
new file mode 100644
index 00000000000..3fc3be27416
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/highlight/issue65516.txt
@@ -0,0 +1,7 @@
+This test checks that gopls doesn't crash while highlighting functions with no
+body (golang/go#65516).
+
+-- p.go --
+package p
+
+func Foo() (int, string) //@highlight("int", "int"), highlight("func", "func")
diff --git a/gopls/internal/test/marker/testdata/hover/embed.txt b/gopls/internal/test/marker/testdata/hover/embed.txt
new file mode 100644
index 00000000000..1dc3fcbfa12
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/hover/embed.txt
@@ -0,0 +1,57 @@
+This test checks that hover reports accessible embedded fields
+(after the doc comment and before the accessible methods).
+
+-- go.mod --
+module example.com
+
+go 1.18
+
+-- q/q.go --
+package q
+
+type Q struct {
+ One int
+ two string
+ q2[chan int]
+}
+
+type q2[T any] struct {
+ Three *T
+ four string
+}
+
+-- p.go --
+package p
+
+import "example.com/q"
+
+// doc
+type P struct {
+ q.Q
+}
+
+func (P) m() {}
+
+var p P //@hover("P", "P", P)
+
+-- @P --
+```go
+type P struct {
+ q.Q
+}
+```
+
+doc
+
+
+```go
+// Embedded fields:
+One int // through Q
+Three *chan int // through Q.q2
+```
+
+```go
+func (P) m()
+```
+
+[`p.P` on pkg.go.dev](https://pkg.go.dev/example.com#P)
diff --git a/gopls/internal/test/marker/testdata/hover/generics.txt b/gopls/internal/test/marker/testdata/hover/generics.txt
index da1042544a0..76c258f3146 100644
--- a/gopls/internal/test/marker/testdata/hover/generics.txt
+++ b/gopls/internal/test/marker/testdata/hover/generics.txt
@@ -1,9 +1,10 @@
This file contains tests for hovering over generic Go code.
-Requires go1.19+ for the new go/doc/comment package.
+Requires go1.20+ for the new go/doc/comment package, and a change in Go 1.20
+that affected the formatting of constraint interfaces.
-- flags --
--min_go=go1.19
+-min_go=go1.20
-- go.mod --
// A go.mod is require for correct pkgsite links.
@@ -25,10 +26,8 @@ type Value[T any] struct { //@hover("T", "T", ValueT)
Q int //@hover("Q", "Q", ValueQ)
}
-// disabled - see issue #54822
-func F[P interface{ ~int | string }]() { // hover("P","P",Ptparam)
- // disabled - see issue #54822
- var _ P // hover("P","P",Pvar)
+func F[P interface{ ~int | string }]() { //@hover("P", "P", Ptparam)
+ var _ P //@hover("P","P",Pvar)
}
-- inferred.go --
@@ -41,7 +40,7 @@ func app[S interface{ ~[]E }, E interface{}](s S, e E) S {
func _() {
_ = app[[]int] //@hover("app", "app", appint)
_ = app[[]int, int] //@hover("app", "app", appint)
- _ = app[[]int]([]int{}, 0) //@hover("app", "app", appint)
+ _ = app[[]int]([]int{}, 0) //@hover("app", "app", appint), diag("[[]int]", re"unnecessary")
_ = app([]int{}, 0) //@hover("app", "app", appint)
}
@@ -76,3 +75,11 @@ field Q int
```go
type parameter T any
```
+-- @Ptparam --
+```go
+type parameter P interface{~int | string}
+```
+-- @Pvar --
+```go
+type parameter P interface{~int | string}
+```
diff --git a/gopls/internal/test/marker/testdata/hover/godef.txt b/gopls/internal/test/marker/testdata/hover/godef.txt
index 9a3af0046fc..2d82ab0debe 100644
--- a/gopls/internal/test/marker/testdata/hover/godef.txt
+++ b/gopls/internal/test/marker/testdata/hover/godef.txt
@@ -114,11 +114,13 @@ var Other Thing
type Thing struct {
Member string //@loc(Member, "Member")
}
+```
-func (Thing).Method(i int) string
-func (*Thing).Method2(i int, j int) (error, string)
-func (Thing).Method3()
-func (*Thing).private()
+```go
+func (t Thing) Method(i int) string
+func (t *Thing) Method2(i int, j int) (error, string)
+func (t Thing) Method3()
+func (t *Thing) private()
```
[`a.Thing` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing)
@@ -128,9 +130,19 @@ type NextThing struct {
Thing
Value int
}
+```
-func (*NextThing).Method3() int
-func (NextThing).another() string
+```go
+// Embedded fields:
+Member string // through Thing
+```
+
+```go
+func (t Thing) Method(i int) string
+func (t *Thing) Method2(i int, j int) (error, string)
+func (n *NextThing) Method3() int
+func (n NextThing) another() string
+func (t *Thing) private()
```
[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/godef.test/a#NextThing)
diff --git a/gopls/internal/test/marker/testdata/hover/linkable.txt b/gopls/internal/test/marker/testdata/hover/linkable.txt
index 2664bd50d3b..b77b9e8a4d9 100644
--- a/gopls/internal/test/marker/testdata/hover/linkable.txt
+++ b/gopls/internal/test/marker/testdata/hover/linkable.txt
@@ -83,6 +83,12 @@ type Local struct {
```
Local types should not be linkable, even if they are capitalized.
+
+
+```go
+// Embedded fields:
+Embed int // through E
+```
-- @Nested --
```go
field Nested int
@@ -103,14 +109,21 @@ type T struct {
Nested int //@hover("Nested", "Nested", Nested)
}
}
-
-func (T).M()
-func (T).m()
```
T is in the package scope, and so should be linkable.
+```go
+// Embedded fields:
+Embed int // through E
+```
+
+```go
+func (T) M()
+func (T) m()
+```
+
[`p.T` on pkg.go.dev](https://pkg.go.dev/mod.com#T)
-- @X --
```go
diff --git a/gopls/internal/test/marker/testdata/hover/linkable_generics.txt b/gopls/internal/test/marker/testdata/hover/linkable_generics.txt
index 0f9f0f06c5d..1ea009e0318 100644
--- a/gopls/internal/test/marker/testdata/hover/linkable_generics.txt
+++ b/gopls/internal/test/marker/testdata/hover/linkable_generics.txt
@@ -66,8 +66,6 @@ func GF[P any](p P)
type GT[P any] struct {
F P //@hover("F", "F", F),hover("P", "P", FP)
}
-
-func (GT[P]).M(p P)
```
Hovering over type parameters should link to documentation.
@@ -75,6 +73,10 @@ Hovering over type parameters should link to documentation.
TODO(rfindley): should it? We should probably link to the type.
+```go
+func (GT[P]) M(p P)
+```
+
[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT)
-- @GTP --
```go
@@ -85,8 +87,6 @@ type parameter P any
type GT[P any] struct {
F P //@hover("F", "F", F),hover("P", "P", FP)
}
-
-func (GT[P]).M(p P)
```
Hovering over type parameters should link to documentation.
@@ -94,6 +94,10 @@ Hovering over type parameters should link to documentation.
TODO(rfindley): should it? We should probably link to the type.
+```go
+func (GT[P]) M(p P)
+```
+
[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT)
-- @M --
```go
@@ -135,8 +139,6 @@ field F int
type GT[P any] struct {
F P //@hover("F", "F", F),hover("P", "P", FP)
}
-
-func (generic.GT[P]).M(p P)
```
Hovering over type parameters should link to documentation.
@@ -144,4 +146,8 @@ Hovering over type parameters should link to documentation.
TODO(rfindley): should it? We should probably link to the type.
+```go
+func (generic.GT[P]) M(p P)
+```
+
[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT)
diff --git a/gopls/internal/test/marker/testdata/hover/methods.txt b/gopls/internal/test/marker/testdata/hover/methods.txt
new file mode 100644
index 00000000000..8af22494f75
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/hover/methods.txt
@@ -0,0 +1,71 @@
+This test checks the formatting of the list of accessible methods.
+
+Observe that:
+- interface methods that appear in the syntax are not repeated
+ in the method set of the type;
+- promoted methods of structs are shown;
+- receiver variables are correctly named;
+- receiver variables have a pointer type if appropriate;
+- only accessible methods are shown.
+
+-- go.mod --
+module example.com
+
+-- lib/lib.go --
+package lib
+
+type I interface {
+ A()
+ b()
+ J
+}
+
+type J interface { C() }
+
+type S struct { I }
+func (s S) A() {}
+func (s S) b() {}
+func (s *S) PA() {}
+func (s *S) pb() {}
+
+-- a/a.go --
+package a
+
+import "example.com/lib"
+
+var _ lib.I //@hover("I", "I", I)
+var _ lib.J //@hover("J", "J", J)
+var _ lib.S //@hover("S", "S", S)
+
+-- @I --
+```go
+type I interface {
+ A()
+ b()
+ J
+}
+```
+
+```go
+func (lib.J) C()
+```
+
+[`lib.I` on pkg.go.dev](https://pkg.go.dev/example.com/lib#I)
+-- @J --
+```go
+type J interface{ C() }
+```
+
+[`lib.J` on pkg.go.dev](https://pkg.go.dev/example.com/lib#J)
+-- @S --
+```go
+type S struct{ I }
+```
+
+```go
+func (s lib.S) A()
+func (lib.J) C()
+func (s *lib.S) PA()
+```
+
+[`lib.S` on pkg.go.dev](https://pkg.go.dev/example.com/lib#S)
diff --git a/gopls/internal/test/marker/testdata/rename/doclink.txt b/gopls/internal/test/marker/testdata/rename/doclink.txt
index 1461f6f13b3..d4e9f96891e 100644
--- a/gopls/internal/test/marker/testdata/rename/doclink.txt
+++ b/gopls/internal/test/marker/testdata/rename/doclink.txt
@@ -1,7 +1,7 @@
This test checks that doc links are also handled correctly (golang/go#64495).
-- go.mod --
-module testdata
+module example.com
go 1.21
@@ -35,7 +35,7 @@ func (E[T]) Foo() {} //@rename("Foo", "Bar", EFooToEBar)
-- b/b.go --
package b
-import aa "testdata/a" //@rename("aa", "a", pkgRename)
+import aa "example.com/a" //@rename("aa", "a", pkgRename)
// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF]
// reference pointer type [*aa.D]
@@ -124,8 +124,8 @@ func FooBar() {
+// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PFF]
-- @pkgRename/b/b.go --
@@ -3 +3 @@
--import aa "testdata/a" //@rename("aa", "a", pkgRename)
-+import "testdata/a" //@rename("aa", "a", pkgRename)
+-import aa "example.com/a" //@rename("aa", "a", pkgRename)
++import "example.com/a" //@rename("aa", "a", pkgRename)
@@ -5,3 +5,3 @@
-// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF]
-// reference pointer type [*aa.D]
diff --git a/gopls/internal/test/marker/testdata/rename/issue42134.txt b/gopls/internal/test/marker/testdata/rename/issue42134.txt
index 6a83762d87c..05fee50bed9 100644
--- a/gopls/internal/test/marker/testdata/rename/issue42134.txt
+++ b/gopls/internal/test/marker/testdata/rename/issue42134.txt
@@ -1,3 +1,5 @@
+Regression test for #42134,
+"rename fails to update doc comment for local variable of function type"
-- 1.go --
package issue42134
@@ -29,7 +31,7 @@ func _() {
fmt.Println(minNumber) //@rename("minNumber", "res", minNumberTores)
}
-func min(a, b int) int { return a }
+func min(a, b int) int { return a + b }
-- @minNumberTores/2.go --
@@ -6 +6 @@
- // minNumber is a min number.
diff --git a/gopls/internal/regtest/marker/testdata/rename/issue60752.txt b/gopls/internal/test/marker/testdata/rename/issue60752.txt
similarity index 100%
rename from gopls/internal/regtest/marker/testdata/rename/issue60752.txt
rename to gopls/internal/test/marker/testdata/rename/issue60752.txt
diff --git a/gopls/internal/test/marker/testdata/signature/issue63804.txt b/gopls/internal/test/marker/testdata/signature/issue63804.txt
index 4ed952956f9..b65183391ef 100644
--- a/gopls/internal/test/marker/testdata/signature/issue63804.txt
+++ b/gopls/internal/test/marker/testdata/signature/issue63804.txt
@@ -4,7 +4,7 @@ the server's Signature method never returns an actual error,
so the best we can assert is that there is no result.
-- go.mod --
-module testdata
+module example.com
go 1.18
-- a/a.go --
diff --git a/gopls/internal/test/marker/testdata/stubmethods/basic.txt b/gopls/internal/test/marker/testdata/stubmethods/basic.txt
index 95b515299a6..e4cfb6d05a0 100644
--- a/gopls/internal/test/marker/testdata/stubmethods/basic.txt
+++ b/gopls/internal/test/marker/testdata/stubmethods/basic.txt
@@ -1,4 +1,5 @@
This test exercises basic 'stub methods' functionality.
+See basic_resolve.txt for the same test with resolve support.
-- go.mod --
module example.com
diff --git a/gopls/internal/test/marker/testdata/stubmethods/basic_resolve.txt b/gopls/internal/test/marker/testdata/stubmethods/basic_resolve.txt
new file mode 100644
index 00000000000..183b7d526eb
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/stubmethods/basic_resolve.txt
@@ -0,0 +1,31 @@
+This test exercises basic 'stub methods' functionality, with resolve support.
+See basic.txt for the same test without resolve support.
+
+-- capabilities.json --
+{
+ "textDocument": {
+ "codeAction": {
+ "dataSupport": true,
+ "resolveSupport": {
+ "properties": ["edit"]
+ }
+ }
+ }
+}
+-- go.mod --
+module example.com
+go 1.12
+
+-- a/a.go --
+package a
+
+type C int
+
+var _ error = C(0) //@suggestedfix(re"C.0.", re"missing method Error", stub)
+-- @stub/a/a.go --
+@@ -5 +5,5 @@
++// Error implements error.
++func (c C) Error() string {
++ panic("unimplemented")
++}
++
diff --git a/gopls/internal/test/marker/testdata/suggestedfix/issue65024.txt b/gopls/internal/test/marker/testdata/suggestedfix/issue65024.txt
new file mode 100644
index 00000000000..afdfce9f1cc
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/suggestedfix/issue65024.txt
@@ -0,0 +1,78 @@
+Regression example.com for #65024, "incorrect package qualification when
+stubbing method in v2 module".
+
+The second test (a-a) ensures that we don't use path-based heuristics
+to guess the PkgName of an import.
+
+-- a/v2/go.mod --
+module example.com/a/v2
+go 1.18
+
+-- a/v2/a.go --
+package a
+
+type I interface { F() T }
+
+type T struct {}
+
+-- a/v2/b/b.go --
+package b
+
+import "example.com/a/v2"
+
+type B struct{}
+
+var _ a.I = &B{} //@ suggestedfix("&B{}", re"does not implement", out)
+
+// This line makes the diff tidier.
+
+-- @out/a/v2/b/b.go --
+@@ -7 +7,5 @@
++// F implements a.I.
++func (b *B) F() a.T {
++ panic("unimplemented")
++}
++
+@@ -10 +15 @@
+-
+-- a-a/v2/go.mod --
+// This module has a hyphenated name--how posh.
+// It won't do to use it as an identifier.
+// The correct name is the one in the package decl,
+// which in this case is not what the path heuristic would guess.
+module example.com/a-a/v2
+go 1.18
+
+-- a-a/v2/a.go --
+package a
+type I interface { F() T }
+type T struct {}
+
+-- a-a/v2/b/b.go --
+package b
+
+// Note: no existing import of a.
+
+type B struct{}
+
+var _ I = &B{} //@ suggestedfix("&B{}", re"does not implement", out2)
+
+// This line makes the diff tidier.
+
+-- a-a/v2/b/import-a-I.go --
+package b
+import "example.com/a-a/v2"
+type I = a.I
+
+-- @out2/a-a/v2/b/b.go --
+@@ -3 +3,2 @@
++import a "example.com/a-a/v2"
++
+@@ -7 +9,5 @@
++// F implements a.I.
++func (b *B) F() a.T {
++ panic("unimplemented")
++}
++
+@@ -10 +17 @@
+-
diff --git a/gopls/internal/test/marker/testdata/suggestedfix/undeclaredfunc.txt b/gopls/internal/test/marker/testdata/suggestedfix/undeclaredfunc.txt
new file mode 100644
index 00000000000..d54dcae073f
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/suggestedfix/undeclaredfunc.txt
@@ -0,0 +1,19 @@
+This test checks the quick fix for "undeclared: f" that declares the
+missing function. See #47558.
+
+TODO(adonovan): infer the result variables from the context (int, in this case).
+
+-- a.go --
+package a
+
+func _() int { return f(1, "") } //@suggestedfix(re"f.1", re"unde(fined|clared name): f", x)
+
+-- @x/a.go --
+@@ -3 +3 @@
+-func _() int { return f(1, "") } //@suggestedfix(re"f.1", re"unde(fined|clared name): f", x)
++func _() int { return f(1, "") }
+@@ -5 +5,4 @@
++func f(i int, s string) {
++ panic("unimplemented")
++} //@suggestedfix(re"f.1", re"unde(fined|clared name): f", x)
++
diff --git a/gopls/internal/util/bug/bug.go b/gopls/internal/util/bug/bug.go
index 7c290b0cd27..e04b753e315 100644
--- a/gopls/internal/util/bug/bug.go
+++ b/gopls/internal/util/bug/bug.go
@@ -19,12 +19,15 @@ import (
"sync"
"time"
- "golang.org/x/telemetry/counter"
+ "golang.org/x/tools/gopls/internal/telemetry"
)
// PanicOnBugs controls whether to panic when bugs are reported.
//
// It may be set to true during testing.
+//
+// TODO(adonovan): should we make the default true, and
+// suppress it only in the product (gopls/main.go)?
var PanicOnBugs = false
var (
@@ -66,7 +69,7 @@ func Report(description string) {
}
// BugReportCount is a telemetry counter that tracks # of bug reports.
-var BugReportCount = counter.NewStack("gopls/bug", 16)
+var BugReportCount = telemetry.NewStackCounter("gopls/bug", 16)
func report(description string) {
_, file, line, ok := runtime.Caller(2) // all exported reporting functions call report directly
diff --git a/gopls/internal/util/immutable/immutable.go b/gopls/internal/util/immutable/immutable.go
index b0b14c16404..a88133fe92f 100644
--- a/gopls/internal/util/immutable/immutable.go
+++ b/gopls/internal/util/immutable/immutable.go
@@ -20,15 +20,6 @@ func MapOf[K comparable, V any](m map[K]V) Map[K, V] {
return Map[K, V]{m}
}
-// Keys returns all keys present in the map.
-func (m Map[K, V]) Keys() []K {
- var keys []K
- for k := range m.m {
- keys = append(keys, k)
- }
- return keys
-}
-
// Value returns the mapped value for k.
// It is equivalent to the commaok form of an ordinary go map, and returns
// (zero, false) if the key is not present.
diff --git a/gopls/internal/util/slices/slices.go b/gopls/internal/util/slices/slices.go
index db53e1d3ff6..4c4fa4d0a79 100644
--- a/gopls/internal/util/slices/slices.go
+++ b/gopls/internal/util/slices/slices.go
@@ -4,6 +4,14 @@
package slices
+// Clone returns a copy of the slice.
+// The elements are copied using assignment, so this is a shallow clone.
+// TODO(rfindley): use go1.19 slices.Clone.
+func Clone[S ~[]E, E any](s S) S {
+ // The s[:0:0] preserves nil in case it matters.
+ return append(s[:0:0], s...)
+}
+
// Contains reports whether x is present in slice.
// TODO(adonovan): use go1.19 slices.Contains.
func Contains[S ~[]E, E comparable](slice S, x E) bool {
@@ -35,7 +43,7 @@ func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
}
// Concat returns a new slice concatenating the passed in slices.
-// TODO(rfindley): use go1.22 slices.Contains.
+// TODO(rfindley): use go1.22 slices.Concat.
func Concat[S ~[]E, E any](slices ...S) S {
size := 0
for _, s := range slices {
@@ -65,3 +73,18 @@ func Grow[S ~[]E, E any](s S, n int) S {
}
return s
}
+
+// Remove removes all values equal to elem from slice.
+//
+// The closest equivalent in the standard slices package is:
+//
+// DeleteFunc(func(x T) bool { return x == elem })
+func Remove[T comparable](slice []T, elem T) []T {
+ out := slice[:0]
+ for _, v := range slice {
+ if v != elem {
+ out = append(out, v)
+ }
+ }
+ return out
+}
diff --git a/gopls/internal/util/typesutil/typesutil.go b/gopls/internal/util/typesutil/typesutil.go
index 137dbf3ba6a..3597b4b4bbc 100644
--- a/gopls/internal/util/typesutil/typesutil.go
+++ b/gopls/internal/util/typesutil/typesutil.go
@@ -21,3 +21,29 @@ func ImportedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, boo
pkgname, ok := obj.(*types.PkgName)
return pkgname, ok
}
+
+// FileQualifier returns a [types.Qualifier] function that qualifies
+// imported symbols appropriately based on the import environment of a
+// given file.
+func FileQualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qualifier {
+ // Construct mapping of import paths to their defined or implicit names.
+ imports := make(map[*types.Package]string)
+ for _, imp := range f.Imports {
+ if pkgname, ok := ImportedPkgName(info, imp); ok {
+ imports[pkgname.Imported()] = pkgname.Name()
+ }
+ }
+ // Define qualifier to replace full package paths with names of the imports.
+ return func(p *types.Package) string {
+ if p == pkg {
+ return ""
+ }
+ if name, ok := imports[p]; ok {
+ if name == "." {
+ return ""
+ }
+ return name
+ }
+ return p.Name()
+ }
+}
diff --git a/gopls/internal/version/version.go b/gopls/internal/version/version.go
new file mode 100644
index 00000000000..96f18190aff
--- /dev/null
+++ b/gopls/internal/version/version.go
@@ -0,0 +1,29 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package version manages the gopls version.
+//
+// The VersionOverride variable may be used to set the gopls version at link
+// time.
+package version
+
+import "runtime/debug"
+
+var VersionOverride = ""
+
+// Version returns the gopls version.
+//
+// By default, this is read from runtime/debug.ReadBuildInfo, but may be
+// overridden by the [VersionOverride] variable.
+func Version() string {
+ if VersionOverride != "" {
+ return VersionOverride
+ }
+ if info, ok := debug.ReadBuildInfo(); ok {
+ if info.Main.Version != "" {
+ return info.Main.Version
+ }
+ }
+ return "(unknown)"
+}
diff --git a/gopls/internal/vulncheck/scan/command.go b/gopls/internal/vulncheck/scan/command.go
index f8d84e3bf38..1f53d8b55f3 100644
--- a/gopls/internal/vulncheck/scan/command.go
+++ b/gopls/internal/vulncheck/scan/command.go
@@ -18,7 +18,7 @@ import (
"time"
"golang.org/x/sync/errgroup"
- "golang.org/x/tools/gopls/internal/lsp/cache"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/vulncheck"
"golang.org/x/tools/gopls/internal/vulncheck/govulncheck"
"golang.org/x/tools/gopls/internal/vulncheck/osv"
diff --git a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go
index 7b637ab890b..659d2f1fd10 100644
--- a/gopls/internal/vulncheck/vulntest/db.go
+++ b/gopls/internal/vulncheck/vulntest/db.go
@@ -19,7 +19,7 @@ import (
"strings"
"time"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/vulncheck/osv"
"golang.org/x/tools/txtar"
)
diff --git a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulncheck/vulntest/db_test.go
index bfab8ca4c59..22281249502 100644
--- a/gopls/internal/vulncheck/vulntest/db_test.go
+++ b/gopls/internal/vulncheck/vulntest/db_test.go
@@ -17,7 +17,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/vulncheck/osv"
)
diff --git a/gopls/internal/work/completion.go b/gopls/internal/work/completion.go
index f8aa20d67bd..194721ef36d 100644
--- a/gopls/internal/work/completion.go
+++ b/gopls/internal/work/completion.go
@@ -14,9 +14,9 @@ import (
"sort"
"strings"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/work/diagnostics.go b/gopls/internal/work/diagnostics.go
index 70147d6a959..f1acd4d27c7 100644
--- a/gopls/internal/work/diagnostics.go
+++ b/gopls/internal/work/diagnostics.go
@@ -11,9 +11,9 @@ import (
"path/filepath"
"golang.org/x/mod/modfile"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/work/format.go b/gopls/internal/work/format.go
index 8ef81f34e2a..162bc8c0004 100644
--- a/gopls/internal/work/format.go
+++ b/gopls/internal/work/format.go
@@ -8,9 +8,9 @@ import (
"context"
"golang.org/x/mod/modfile"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/internal/work/hover.go b/gopls/internal/work/hover.go
index 66c40a81f29..c59c14789be 100644
--- a/gopls/internal/work/hover.go
+++ b/gopls/internal/work/hover.go
@@ -10,9 +10,9 @@ import (
"fmt"
"golang.org/x/mod/modfile"
+ "golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/file"
- "golang.org/x/tools/gopls/internal/lsp/cache"
- "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
diff --git a/gopls/main.go b/gopls/main.go
index 8163266972f..e3fb3861f68 100644
--- a/gopls/main.go
+++ b/gopls/main.go
@@ -17,14 +17,20 @@ import (
"context"
"os"
- "golang.org/x/telemetry/counter"
"golang.org/x/tools/gopls/internal/cmd"
"golang.org/x/tools/gopls/internal/hooks"
+ "golang.org/x/tools/gopls/internal/telemetry"
+ versionpkg "golang.org/x/tools/gopls/internal/version"
"golang.org/x/tools/internal/tool"
)
+var version = "" // if set by the linker, overrides the gopls version
+
func main() {
- counter.Open() // Enable telemetry counter writing.
+ versionpkg.VersionOverride = version
+
+ telemetry.CounterOpen()
+ telemetry.StartCrashMonitor()
ctx := context.Background()
- tool.Main(ctx, cmd.New("gopls", "", nil, hooks.Options), os.Args[1:])
+ tool.Main(ctx, cmd.New(hooks.Options), os.Args[1:])
}
diff --git a/internal/aliases/aliases.go b/internal/aliases/aliases.go
new file mode 100644
index 00000000000..f89112c8ee5
--- /dev/null
+++ b/internal/aliases/aliases.go
@@ -0,0 +1,28 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package aliases
+
+import (
+ "go/token"
+ "go/types"
+)
+
+// Package aliases defines backward compatible shims
+// for the types.Alias type representation added in 1.22.
+// This defines placeholders for x/tools until 1.26.
+
+// NewAlias creates a new TypeName in Package pkg that
+// is an alias for the type rhs.
+//
+// When GoVersion>=1.22 and GODEBUG=gotypesalias=1,
+// the Type() of the return value is a *types.Alias.
+func NewAlias(pos token.Pos, pkg *types.Package, name string, rhs types.Type) *types.TypeName {
+ if enabled() {
+ tname := types.NewTypeName(pos, pkg, name, nil)
+ newAlias(tname, rhs)
+ return tname
+ }
+ return types.NewTypeName(pos, pkg, name, rhs)
+}
diff --git a/internal/aliases/aliases_go121.go b/internal/aliases/aliases_go121.go
new file mode 100644
index 00000000000..1872b56ff8f
--- /dev/null
+++ b/internal/aliases/aliases_go121.go
@@ -0,0 +1,30 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build !go1.22
+// +build !go1.22
+
+package aliases
+
+import (
+ "go/types"
+)
+
+// Alias is a placeholder for a go/types.Alias for <=1.21.
+// It will never be created by go/types.
+type Alias struct{}
+
+func (*Alias) String() string { panic("unreachable") }
+
+func (*Alias) Underlying() types.Type { panic("unreachable") }
+
+func (*Alias) Obj() *types.TypeName { panic("unreachable") }
+
+// Unalias returns the type t for go <=1.21.
+func Unalias(t types.Type) types.Type { return t }
+
+// Always false for go <=1.21. Ignores GODEBUG.
+func enabled() bool { return false }
+
+func newAlias(name *types.TypeName, rhs types.Type) *Alias { panic("unreachable") }
diff --git a/internal/aliases/aliases_go122.go b/internal/aliases/aliases_go122.go
new file mode 100644
index 00000000000..8b92116284d
--- /dev/null
+++ b/internal/aliases/aliases_go122.go
@@ -0,0 +1,72 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.22
+// +build go1.22
+
+package aliases
+
+import (
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "os"
+ "strings"
+ "sync"
+)
+
+// Alias is an alias of types.Alias.
+type Alias = types.Alias
+
+// Unalias is a wrapper of types.Unalias.
+func Unalias(t types.Type) types.Type { return types.Unalias(t) }
+
+// newAlias is an internal alias around types.NewAlias.
+// Direct usage is discouraged as the moment.
+// Try to use NewAlias instead.
+func newAlias(tname *types.TypeName, rhs types.Type) *Alias {
+ a := types.NewAlias(tname, rhs)
+ // TODO(go.dev/issue/65455): Remove kludgy workaround to set a.actual as a side-effect.
+ Unalias(a)
+ return a
+}
+
+// enabled returns true when types.Aliases are enabled.
+func enabled() bool {
+ // Use the gotypesalias value in GODEBUG if set.
+ godebug := os.Getenv("GODEBUG")
+ value := -1 // last set value.
+ for _, f := range strings.Split(godebug, ",") {
+ switch f {
+ case "gotypesalias=1":
+ value = 1
+ case "gotypesalias=0":
+ value = 0
+ }
+ }
+ switch value {
+ case 0:
+ return false
+ case 1:
+ return true
+ default:
+ return aliasesDefault()
+ }
+}
+
+// aliasesDefault reports if aliases are enabled by default.
+func aliasesDefault() bool {
+ // Dynamically check if Aliases will be produced from go/types.
+ aliasesDefaultOnce.Do(func() {
+ fset := token.NewFileSet()
+ f, _ := parser.ParseFile(fset, "a.go", "package p; type A = int", 0)
+ pkg, _ := new(types.Config).Check("p", fset, []*ast.File{f}, nil)
+ _, gotypesaliasDefault = pkg.Scope().Lookup("A").Type().(*types.Alias)
+ })
+ return gotypesaliasDefault
+}
+
+var gotypesaliasDefault bool
+var aliasesDefaultOnce sync.Once
diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go
new file mode 100644
index 00000000000..fc4efb2cb27
--- /dev/null
+++ b/internal/aliases/aliases_test.go
@@ -0,0 +1,75 @@
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package aliases_test
+
+import (
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "os"
+ "testing"
+
+ "golang.org/x/tools/internal/aliases"
+ "golang.org/x/tools/internal/testenv"
+)
+
+// Assert that Obj exists on Alias.
+var _ func(*aliases.Alias) *types.TypeName = (*aliases.Alias).Obj
+
+// TestNewAlias tests that alias.NewAlias creates an alias of a type
+// whose underlying and Unaliased type is *Named.
+// When gotypesalias=1 and GoVersion >= 1.22, the type will
+// be an *aliases.Alias.
+func TestNewAlias(t *testing.T) {
+ const source = `
+ package P
+
+ type Named int
+ `
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, "hello.go", source, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var conf types.Config
+ pkg, err := conf.Check("P", fset, []*ast.File{f}, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expr := `*Named`
+ tv, err := types.Eval(fset, pkg, 0, expr)
+ if err != nil {
+ t.Fatalf("Eval(%s) failed: %v", expr, err)
+ }
+
+ for _, godebug := range []string{"", "gotypesalias=1"} {
+ t.Run(godebug, func(t *testing.T) {
+ saved := os.Getenv("GODEBUG")
+ defer os.Setenv("GODEBUG", saved)
+ os.Setenv("GODEBUG", godebug) // non parallel.
+
+ A := aliases.NewAlias(token.NoPos, pkg, "A", tv.Type)
+ if got, want := A.Name(), "A"; got != want {
+ t.Errorf("Expected A.Name()==%q. got %q", want, got)
+ }
+
+ if got, want := A.Type().Underlying(), tv.Type; got != want {
+ t.Errorf("Expected A.Type().Underlying()==%q. got %q", want, got)
+ }
+ if got, want := aliases.Unalias(A.Type()), tv.Type; got != want {
+ t.Errorf("Expected Unalias(A)==%q. got %q", want, got)
+ }
+
+ if testenv.Go1Point() >= 22 && godebug == "gotypesalias=1" {
+ if _, ok := A.Type().(*aliases.Alias); !ok {
+ t.Errorf("Expected A.Type() to be a types.Alias(). got %q", A.Type())
+ }
+ }
+ })
+ }
+}
diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go
index 2b291680479..b24a0fba9e7 100644
--- a/internal/analysisinternal/analysis.go
+++ b/internal/analysisinternal/analysis.go
@@ -151,6 +151,10 @@ func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr {
},
})
}
+ if t.Variadic() {
+ last := params[len(params)-1]
+ last.Type = &ast.Ellipsis{Elt: last.Type.(*ast.ArrayType).Elt}
+ }
var returns []*ast.Field
for i := 0; i < t.Results().Len(); i++ {
r := TypeExpr(f, pkg, t.Results().At(i).Type())
diff --git a/internal/apidiff/apidiff.go b/internal/apidiff/apidiff.go
index 873ee85fbc4..087e112e599 100644
--- a/internal/apidiff/apidiff.go
+++ b/internal/apidiff/apidiff.go
@@ -19,6 +19,8 @@ import (
"go/constant"
"go/token"
"go/types"
+
+ "golang.org/x/tools/internal/aliases"
)
// Changes reports on the differences between the APIs of the old and new packages.
@@ -206,7 +208,7 @@ func (d *differ) typeChanged(obj types.Object, part string, old, new types.Type)
// Since these can change without affecting compatibility, we don't want users to
// be distracted by them, so we remove them.
func removeNamesFromSignature(t types.Type) types.Type {
- sig, ok := t.(*types.Signature)
+ sig, ok := aliases.Unalias(t).(*types.Signature)
if !ok {
return t
}
diff --git a/internal/apidiff/compatibility.go b/internal/apidiff/compatibility.go
index 2e327485b52..0d2d2b34575 100644
--- a/internal/apidiff/compatibility.go
+++ b/internal/apidiff/compatibility.go
@@ -8,9 +8,13 @@ import (
"fmt"
"go/types"
"reflect"
+
+ "golang.org/x/tools/internal/aliases"
)
func (d *differ) checkCompatible(otn *types.TypeName, old, new types.Type) {
+ old = aliases.Unalias(old)
+ new = aliases.Unalias(new)
switch old := old.(type) {
case *types.Interface:
if new, ok := new.(*types.Interface); ok {
@@ -268,7 +272,7 @@ func (d *differ) checkCompatibleDefined(otn *types.TypeName, old *types.Named, n
return
}
// Interface method sets are checked in checkCompatibleInterface.
- if _, ok := old.Underlying().(*types.Interface); ok {
+ if types.IsInterface(old) {
return
}
@@ -287,7 +291,7 @@ func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addc
oldMethodSet := exportedMethods(oldt)
newMethodSet := exportedMethods(newt)
msname := otn.Name()
- if _, ok := oldt.(*types.Pointer); ok {
+ if _, ok := aliases.Unalias(oldt).(*types.Pointer); ok {
msname = "*" + msname
}
for name, oldMethod := range oldMethodSet {
@@ -349,9 +353,9 @@ func receiverType(method types.Object) types.Type {
}
func receiverNamedType(method types.Object) *types.Named {
- switch t := receiverType(method).(type) {
+ switch t := aliases.Unalias(receiverType(method)).(type) {
case *types.Pointer:
- return t.Elem().(*types.Named)
+ return aliases.Unalias(t.Elem()).(*types.Named)
case *types.Named:
return t
default:
@@ -360,6 +364,6 @@ func receiverNamedType(method types.Object) *types.Named {
}
func hasPointerReceiver(method types.Object) bool {
- _, ok := receiverType(method).(*types.Pointer)
+ _, ok := aliases.Unalias(receiverType(method)).(*types.Pointer)
return ok
}
diff --git a/internal/apidiff/correspondence.go b/internal/apidiff/correspondence.go
index 0d7b4c5a5f1..dd2f5178173 100644
--- a/internal/apidiff/correspondence.go
+++ b/internal/apidiff/correspondence.go
@@ -7,6 +7,8 @@ package apidiff
import (
"go/types"
"sort"
+
+ "golang.org/x/tools/internal/aliases"
)
// Two types are correspond if they are identical except for defined types,
@@ -31,6 +33,8 @@ func (d *differ) correspond(old, new types.Type) bool {
// Compare this to the implementation of go/types.Identical.
func (d *differ) corr(old, new types.Type, p *ifacePair) bool {
// Structure copied from types.Identical.
+ old = aliases.Unalias(old)
+ new = aliases.Unalias(new)
switch old := old.(type) {
case *types.Basic:
return types.Identical(old, new)
diff --git a/internal/diff/lcs/git.sh b/internal/diff/lcs/git.sh
index 6856f843958..b25ba4aac74 100644
--- a/internal/diff/lcs/git.sh
+++ b/internal/diff/lcs/git.sh
@@ -14,9 +14,9 @@ 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
+# file=internal/golang/completion/completion.go
+# file=internal/golang/diagnostics.go
+file=internal/protocol/tsprotocol.go
tmp=$(mktemp -d)
git log $file |
diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go
index 9bde15e3bc6..9fffa9ad05c 100644
--- a/internal/gcimporter/iimport.go
+++ b/internal/gcimporter/iimport.go
@@ -224,6 +224,7 @@ func iimportCommon(fset *token.FileSet, getPackages GetPackagesFunc, data []byte
// Gather the relevant packages from the manifest.
items := make([]GetPackagesItem, r.uint64())
+ uniquePkgPaths := make(map[string]bool)
for i := range items {
pkgPathOff := r.uint64()
pkgPath := p.stringAt(pkgPathOff)
@@ -248,6 +249,12 @@ func iimportCommon(fset *token.FileSet, getPackages GetPackagesFunc, data []byte
}
items[i].nameIndex = nameIndex
+
+ uniquePkgPaths[pkgPath] = true
+ }
+ // Debugging #63822; hypothesis: there are duplicate PkgPaths.
+ if len(uniquePkgPaths) != len(items) {
+ reportf("found duplicate PkgPaths while reading export data manifest: %v", items)
}
// Request packages all at once from the client,
diff --git a/internal/gopathwalk/walk.go b/internal/gopathwalk/walk.go
index 52f74e643be..8361515519f 100644
--- a/internal/gopathwalk/walk.go
+++ b/internal/gopathwalk/walk.go
@@ -9,11 +9,13 @@ package gopathwalk
import (
"bufio"
"bytes"
+ "io"
"io/fs"
- "log"
"os"
"path/filepath"
+ "runtime"
"strings"
+ "sync"
"time"
)
@@ -21,8 +23,13 @@ import (
type Options struct {
// If Logf is non-nil, debug logging is enabled through this function.
Logf func(format string, args ...interface{})
+
// Search module caches. Also disables legacy goimports ignore rules.
ModulesEnabled bool
+
+ // Maximum number of concurrent calls to user-provided callbacks,
+ // or 0 for GOMAXPROCS.
+ Concurrency int
}
// RootType indicates the type of a Root.
@@ -43,19 +50,28 @@ type Root struct {
Type RootType
}
-// Walk walks Go source directories ($GOROOT, $GOPATH, etc) to find packages.
+// Walk concurrently walks Go source directories ($GOROOT, $GOPATH, etc) to find packages.
+//
// For each package found, add will be called with the absolute
// paths of the containing source directory and the package directory.
+//
+// Unlike filepath.WalkDir, Walk follows symbolic links
+// (while guarding against cycles).
func Walk(roots []Root, add func(root Root, dir string), opts Options) {
WalkSkip(roots, add, func(Root, string) bool { return false }, opts)
}
-// WalkSkip walks Go source directories ($GOROOT, $GOPATH, etc) to find packages.
+// WalkSkip concurrently walks Go source directories ($GOROOT, $GOPATH, etc) to
+// find packages.
+//
// For each package found, add will be called with the absolute
// paths of the containing source directory and the package directory.
// For each directory that will be scanned, skip will be called
// with the absolute paths of the containing source directory and the directory.
// If skip returns false on a directory it will be processed.
+//
+// Unlike filepath.WalkDir, WalkSkip follows symbolic links
+// (while guarding against cycles).
func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root, dir string) bool, opts Options) {
for _, root := range roots {
walkDir(root, add, skip, opts)
@@ -64,45 +80,51 @@ func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root
// walkDir creates a walker and starts fastwalk with this walker.
func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) bool, opts Options) {
+ if opts.Logf == nil {
+ opts.Logf = func(format string, args ...interface{}) {}
+ }
if _, err := os.Stat(root.Path); os.IsNotExist(err) {
- if opts.Logf != nil {
- opts.Logf("skipping nonexistent directory: %v", root.Path)
- }
+ opts.Logf("skipping nonexistent directory: %v", root.Path)
return
}
start := time.Now()
- if opts.Logf != nil {
- opts.Logf("scanning %s", root.Path)
+ opts.Logf("scanning %s", root.Path)
+
+ concurrency := opts.Concurrency
+ if concurrency == 0 {
+ // The walk be either CPU-bound or I/O-bound, depending on what the
+ // caller-supplied add function does and the details of the user's platform
+ // and machine. Rather than trying to fine-tune the concurrency level for a
+ // specific environment, we default to GOMAXPROCS: it is likely to be a good
+ // choice for a CPU-bound add function, and if it is instead I/O-bound, then
+ // dealing with I/O saturation is arguably the job of the kernel and/or
+ // runtime. (Oversaturating I/O seems unlikely to harm performance as badly
+ // as failing to saturate would.)
+ concurrency = runtime.GOMAXPROCS(0)
}
-
w := &walker{
- root: root,
- add: add,
- skip: skip,
- opts: opts,
- added: make(map[string]bool),
+ root: root,
+ add: add,
+ skip: skip,
+ opts: opts,
+ sem: make(chan struct{}, concurrency),
}
w.init()
- // Add a trailing path separator to cause filepath.WalkDir to traverse symlinks.
+ w.sem <- struct{}{}
path := root.Path
- if len(path) == 0 {
- path = "." + string(filepath.Separator)
- } else if !os.IsPathSeparator(path[len(path)-1]) {
- path = path + string(filepath.Separator)
+ if path == "" {
+ path = "."
}
-
- if err := filepath.WalkDir(path, w.walk); err != nil {
- logf := opts.Logf
- if logf == nil {
- logf = log.Printf
- }
- logf("scanning directory %v: %v", root.Path, err)
+ if fi, err := os.Lstat(path); err == nil {
+ w.walk(path, nil, fs.FileInfoToDirEntry(fi))
+ } else {
+ w.opts.Logf("scanning directory %v: %v", root.Path, err)
}
+ <-w.sem
+ w.walking.Wait()
- if opts.Logf != nil {
- opts.Logf("scanned %s in %v", root.Path, time.Since(start))
- }
+ opts.Logf("scanned %s in %v", root.Path, time.Since(start))
}
// walker is the callback for fastwalk.Walk.
@@ -112,10 +134,18 @@ type walker struct {
skip func(Root, string) bool // The callback that will be invoked for every dir. dir is skipped if it returns true.
opts Options // Options passed to Walk by the user.
- pathSymlinks []os.FileInfo
- ignoredDirs []string
+ walking sync.WaitGroup
+ sem chan struct{} // Channel of semaphore tokens; send to acquire, receive to release.
+ ignoredDirs []string
- added map[string]bool
+ added sync.Map // map[string]bool
+}
+
+// A symlinkList is a linked list of os.FileInfos for parent directories
+// reached via symlinks.
+type symlinkList struct {
+ info os.FileInfo
+ prev *symlinkList
}
// init initializes the walker based on its Options
@@ -132,9 +162,7 @@ func (w *walker) init() {
for _, p := range ignoredPaths {
full := filepath.Join(w.root.Path, p)
w.ignoredDirs = append(w.ignoredDirs, full)
- if w.opts.Logf != nil {
- w.opts.Logf("Directory added to ignore list: %s", full)
- }
+ w.opts.Logf("Directory added to ignore list: %s", full)
}
}
@@ -144,12 +172,10 @@ func (w *walker) init() {
func (w *walker) getIgnoredDirs(path string) []string {
file := filepath.Join(path, ".goimportsignore")
slurp, err := os.ReadFile(file)
- if w.opts.Logf != nil {
- if err != nil {
- w.opts.Logf("%v", err)
- } else {
- w.opts.Logf("Read %s", file)
- }
+ if err != nil {
+ w.opts.Logf("%v", err)
+ } else {
+ w.opts.Logf("Read %s", file)
}
if err != nil {
return nil
@@ -183,149 +209,129 @@ func (w *walker) shouldSkipDir(dir string) bool {
// walk walks through the given path.
//
-// Errors are logged if w.opts.Logf is non-nil, but otherwise ignored:
-// walk returns only nil or fs.SkipDir.
-func (w *walker) walk(path string, d fs.DirEntry, err error) error {
- if err != nil {
- // We have no way to report errors back through Walk or WalkSkip,
- // so just log and ignore them.
- if w.opts.Logf != nil {
+// Errors are logged if w.opts.Logf is non-nil, but otherwise ignored.
+func (w *walker) walk(path string, pathSymlinks *symlinkList, d fs.DirEntry) {
+ if d.Type()&os.ModeSymlink != 0 {
+ // Walk the symlink's target rather than the symlink itself.
+ //
+ // (Note that os.Stat, unlike the lower-lever os.Readlink,
+ // follows arbitrarily many layers of symlinks, so it will eventually
+ // reach either a non-symlink or a nonexistent target.)
+ //
+ // TODO(bcmills): 'go list all' itself ignores symlinks within GOROOT/src
+ // and GOPATH/src. Do we really need to traverse them here? If so, why?
+
+ fi, err := os.Stat(path)
+ if err != nil {
w.opts.Logf("%v", err)
+ return
+ }
+
+ // Avoid walking symlink cycles: if we have already followed a symlink to
+ // this directory as a parent of itself, don't follow it again.
+ //
+ // This doesn't catch the first time through a cycle, but it also minimizes
+ // the number of extra stat calls we make if we *don't* encounter a cycle.
+ // Since we don't actually expect to encounter symlink cycles in practice,
+ // this seems like the right tradeoff.
+ for parent := pathSymlinks; parent != nil; parent = parent.prev {
+ if os.SameFile(fi, parent.info) {
+ return
+ }
}
- if d == nil {
- // Nothing more to do: the error prevents us from knowing
- // what path even represents.
- return nil
+
+ pathSymlinks = &symlinkList{
+ info: fi,
+ prev: pathSymlinks,
}
+ d = fs.FileInfoToDirEntry(fi)
}
if d.Type().IsRegular() {
if !strings.HasSuffix(path, ".go") {
- return nil
+ return
}
dir := filepath.Dir(path)
if dir == w.root.Path && (w.root.Type == RootGOROOT || w.root.Type == RootGOPATH) {
// Doesn't make sense to have regular files
// directly in your $GOPATH/src or $GOROOT/src.
- return nil
+ //
+ // TODO(bcmills): there are many levels of directory within
+ // RootModuleCache where this also wouldn't make sense,
+ // Can we generalize this to any directory without a corresponding
+ // import path?
+ return
}
- if !w.added[dir] {
+ if _, dup := w.added.LoadOrStore(dir, true); !dup {
w.add(w.root, dir)
- w.added[dir] = true
}
- return nil
}
- if d.IsDir() {
- base := filepath.Base(path)
- if base == "" || base[0] == '.' || base[0] == '_' ||
- base == "testdata" ||
- (w.root.Type == RootGOROOT && w.opts.ModulesEnabled && base == "vendor") ||
- (!w.opts.ModulesEnabled && base == "node_modules") {
- return fs.SkipDir
- }
- if w.shouldSkipDir(path) {
- return fs.SkipDir
- }
- return nil
+ if !d.IsDir() {
+ return
}
- if d.Type()&os.ModeSymlink != 0 {
- // TODO(bcmills): 'go list all' itself ignores symlinks within GOROOT/src
- // and GOPATH/src. Do we really need to traverse them here? If so, why?
-
- fi, err := os.Stat(path)
- if err != nil || !fi.IsDir() {
- // Not a directory. Just walk the file (or broken link) and be done.
- return w.walk(path, fs.FileInfoToDirEntry(fi), err)
- }
-
- // Avoid walking symlink cycles: if we have already followed a symlink to
- // this directory as a parent of itself, don't follow it again.
- //
- // This doesn't catch the first time through a cycle, but it also minimizes
- // the number of extra stat calls we make if we *don't* encounter a cycle.
- // Since we don't actually expect to encounter symlink cycles in practice,
- // this seems like the right tradeoff.
- for _, parent := range w.pathSymlinks {
- if os.SameFile(fi, parent) {
- return nil
- }
- }
+ base := filepath.Base(path)
+ if base == "" || base[0] == '.' || base[0] == '_' ||
+ base == "testdata" ||
+ (w.root.Type == RootGOROOT && w.opts.ModulesEnabled && base == "vendor") ||
+ (!w.opts.ModulesEnabled && base == "node_modules") ||
+ w.shouldSkipDir(path) {
+ return
+ }
- w.pathSymlinks = append(w.pathSymlinks, fi)
- defer func() {
- w.pathSymlinks = w.pathSymlinks[:len(w.pathSymlinks)-1]
- }()
+ // Read the directory and walk its entries.
- // On some platforms the OS (or the Go os package) sometimes fails to
- // resolve directory symlinks before a trailing slash
- // (even though POSIX requires it to do so).
- //
- // On macOS that failure may be caused by a known libc/kernel bug;
- // see https://go.dev/issue/59586.
- //
- // On Windows before Go 1.21, it may be caused by a bug in
- // os.Lstat (fixed in https://go.dev/cl/463177).
- //
- // Since we need to handle this explicitly on broken platforms anyway,
- // it is simplest to just always do that and not rely on POSIX pathname
- // resolution to walk the directory (such as by calling WalkDir with
- // a trailing slash appended to the path).
+ f, err := os.Open(path)
+ if err != nil {
+ w.opts.Logf("%v", err)
+ return
+ }
+ defer f.Close()
+
+ for {
+ // We impose an arbitrary limit on the number of ReadDir results per
+ // directory to limit the amount of memory consumed for stale or upcoming
+ // directory entries. The limit trades off CPU (number of syscalls to read
+ // the whole directory) against RAM (reachable directory entries other than
+ // the one currently being processed).
//
- // Instead, we make a sequence of walk calls — directly and through
- // recursive calls to filepath.WalkDir — simulating what WalkDir would do
- // if the symlink were a regular directory.
-
- // First we call walk on the path as a directory
- // (instead of a symlink).
- err = w.walk(path, fs.FileInfoToDirEntry(fi), nil)
- if err == fs.SkipDir {
- return nil
- } else if err != nil {
- // This should be impossible, but handle it anyway in case
- // walk is changed to return other errors.
- return err
- }
-
- // Now read the directory and walk its entries.
- ents, err := os.ReadDir(path)
+ // Since we process the directories recursively, we will end up maintaining
+ // a slice of entries for each level of the directory tree.
+ // (Compare https://go.dev/issue/36197.)
+ ents, err := f.ReadDir(1024)
if err != nil {
- // Report the ReadDir error, as filepath.WalkDir would do.
- err = w.walk(path, fs.FileInfoToDirEntry(fi), err)
- if err == fs.SkipDir {
- return nil
- } else if err != nil {
- return err // Again, should be impossible.
+ if err != io.EOF {
+ w.opts.Logf("%v", err)
}
- // Fall through and iterate over whatever entries we did manage to get.
+ break
}
for _, d := range ents {
nextPath := filepath.Join(path, d.Name())
if d.IsDir() {
- // We want to walk the whole directory tree rooted at nextPath,
- // not just the single entry for the directory.
- err := filepath.WalkDir(nextPath, w.walk)
- if err != nil && w.opts.Logf != nil {
- w.opts.Logf("%v", err)
- }
- } else {
- err := w.walk(nextPath, d, nil)
- if err == fs.SkipDir {
- // Skip the rest of the entries in the parent directory of nextPath
- // (that is, path itself).
- break
- } else if err != nil {
- return err // Again, should be impossible.
+ select {
+ case w.sem <- struct{}{}:
+ // Got a new semaphore token, so we can traverse the directory concurrently.
+ d := d
+ w.walking.Add(1)
+ go func() {
+ defer func() {
+ <-w.sem
+ w.walking.Done()
+ }()
+ w.walk(nextPath, pathSymlinks, d)
+ }()
+ continue
+
+ default:
+ // No tokens available, so traverse serially.
}
}
+
+ w.walk(nextPath, pathSymlinks, d)
}
- return nil
}
-
- // Not a file, regular directory, or symlink; skip.
- return nil
}
diff --git a/internal/gopathwalk/walk_test.go b/internal/gopathwalk/walk_test.go
index 51719dbbcd9..8028f818588 100644
--- a/internal/gopathwalk/walk_test.go
+++ b/internal/gopathwalk/walk_test.go
@@ -37,17 +37,19 @@ func TestSymlinkTraversal(t *testing.T) {
t.Fatal(err)
}
- pkgs := []string{}
+ pkgc := make(chan []string, 1)
+ pkgc <- nil
add := func(root Root, dir string) {
rel, err := filepath.Rel(filepath.Join(root.Path, "src"), dir)
if err != nil {
t.Error(err)
}
- pkgs = append(pkgs, filepath.ToSlash(rel))
+ pkgc <- append(<-pkgc, filepath.ToSlash(rel))
}
Walk([]Root{{Path: gopath, Type: RootGOPATH}}, add, Options{Logf: t.Logf})
+ pkgs := <-pkgc
sort.Strings(pkgs)
t.Logf("Found packages:\n\t%s", strings.Join(pkgs, "\n\t"))
diff --git a/internal/imports/fix.go b/internal/imports/fix.go
index dd369c072e0..6a18f63a44d 100644
--- a/internal/imports/fix.go
+++ b/internal/imports/fix.go
@@ -13,6 +13,7 @@ import (
"go/build"
"go/parser"
"go/token"
+ "go/types"
"io/fs"
"io/ioutil"
"os"
@@ -700,20 +701,21 @@ func ScoreImportPaths(ctx context.Context, env *ProcessEnv, paths []string) (map
return result, nil
}
-func PrimeCache(ctx context.Context, env *ProcessEnv) error {
+func PrimeCache(ctx context.Context, resolver Resolver) error {
// Fully scan the disk for directories, but don't actually read any Go files.
callback := &scanCallback{
- rootFound: func(gopathwalk.Root) bool {
- return true
+ rootFound: func(root gopathwalk.Root) bool {
+ // See getCandidatePkgs: walking GOROOT is apparently expensive and
+ // unnecessary.
+ return root.Type != gopathwalk.RootGOROOT
},
dirFound: func(pkg *pkg) bool {
return false
},
- packageNameLoaded: func(pkg *pkg) bool {
- return false
- },
+ // packageNameLoaded and exportsLoaded must never be called.
}
- return getCandidatePkgs(ctx, callback, "", "", env)
+
+ return resolver.scan(ctx, callback)
}
func candidateImportName(pkg *pkg) string {
@@ -827,16 +829,45 @@ 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", "GOWORK"}
+// TODO(rfindley): we should depend on GOOS and GOARCH, to provide accurate
+// imports when doing cross-platform development.
+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.
+//
+// ...a ProcessEnv *also* overwrites its Env along with derived state in the
+// form of the resolver. And because it is lazily initialized, an env may just
+// be broken and unusable, but there is no way for the caller to detect that:
+// all queries will just fail.
+//
+// TODO(rfindley): refactor this package so that this type (perhaps renamed to
+// just Env or Config) is an immutable configuration struct, to be exchanged
+// for an initialized object via a constructor that returns an error. Perhaps
+// the signature should be `func NewResolver(*Env) (*Resolver, error)`, where
+// resolver is a concrete type used for resolving imports. Via this
+// refactoring, we can avoid the need to call ProcessEnv.init and
+// ProcessEnv.GoEnv everywhere, and implicitly fix all the places where this
+// these are misused. Also, we'd delegate the caller the decision of how to
+// handle a broken environment.
type ProcessEnv struct {
GocmdRunner *gocommand.Runner
BuildFlags []string
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,
@@ -846,7 +877,7 @@ type ProcessEnv struct {
// 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.
- // Specifying all of RequiredGoEnvVars avoids a call to `go env`.
+ // Specifying all of requiredGoEnvVars avoids a call to `go env`.
Env map[string]string
WorkingDir string
@@ -854,9 +885,17 @@ type ProcessEnv struct {
// If Logf is non-nil, debug logging is enabled through this function.
Logf func(format string, args ...interface{})
- initialized bool
+ // If set, ModCache holds a shared cache of directory info to use across
+ // multiple ProcessEnvs.
+ ModCache *DirInfoCache
- resolver Resolver
+ initialized bool // see TODO above
+
+ // resolver and resolverErr are lazily evaluated (see GetResolver).
+ // This is unclean, but see the big TODO in the docstring for ProcessEnv
+ // above: for now, we can't be sure that the ProcessEnv is fully initialized.
+ resolver Resolver
+ resolverErr error
}
func (e *ProcessEnv) goEnv() (map[string]string, error) {
@@ -936,20 +975,31 @@ func (e *ProcessEnv) env() []string {
}
func (e *ProcessEnv) GetResolver() (Resolver, error) {
- if e.resolver != nil {
- return e.resolver, nil
- }
if err := e.init(); err != nil {
return nil, err
}
- if len(e.Env["GOMOD"]) == 0 && len(e.Env["GOWORK"]) == 0 {
- e.resolver = newGopathResolver(e)
- return e.resolver, nil
+
+ if e.resolver == nil && e.resolverErr == nil {
+ // TODO(rfindley): we should only use a gopathResolver here if the working
+ // directory is actually *in* GOPATH. (I seem to recall an open gopls issue
+ // for this behavior, but I can't find it).
+ //
+ // For gopls, we can optionally explicitly choose a resolver type, since we
+ // already know the view type.
+ if len(e.Env["GOMOD"]) == 0 && len(e.Env["GOWORK"]) == 0 {
+ e.resolver = newGopathResolver(e)
+ } else {
+ e.resolver, e.resolverErr = newModuleResolver(e, e.ModCache)
+ }
}
- e.resolver = newModuleResolver(e)
- return e.resolver, nil
+
+ return e.resolver, e.resolverErr
}
+// buildContext returns the build.Context to use for matching files.
+//
+// TODO(rfindley): support dynamic GOOS, GOARCH here, when doing cross-platform
+// development.
func (e *ProcessEnv) buildContext() (*build.Context, error) {
ctx := build.Default
goenv, err := e.goEnv()
@@ -1029,15 +1079,23 @@ func addStdlibCandidates(pass *pass, refs references) error {
type Resolver interface {
// loadPackageNames loads the package names in importPaths.
loadPackageNames(importPaths []string, srcDir string) (map[string]string, error)
+
// scan works with callback to search for packages. See scanCallback for details.
scan(ctx context.Context, callback *scanCallback) error
+
// loadExports returns the set of exported symbols in the package at dir.
// loadExports may be called concurrently.
loadExports(ctx context.Context, pkg *pkg, includeTest bool) (string, []string, error)
+
// scoreImportPath returns the relevance for an import path.
scoreImportPath(ctx context.Context, path string) float64
- ClearForNewScan()
+ // ClearForNewScan returns a new Resolver based on the receiver that has
+ // cleared its internal caches of directory contents.
+ //
+ // The new resolver should be primed and then set via
+ // [ProcessEnv.UpdateResolver].
+ ClearForNewScan() Resolver
}
// A scanCallback controls a call to scan and receives its results.
@@ -1120,7 +1178,7 @@ func addExternalCandidates(ctx context.Context, pass *pass, refs references, fil
go func(pkgName string, symbols map[string]bool) {
defer wg.Done()
- found, err := findImport(ctx, pass, found[pkgName], pkgName, symbols, filename)
+ found, err := findImport(ctx, pass, found[pkgName], pkgName, symbols)
if err != nil {
firstErrOnce.Do(func() {
@@ -1151,6 +1209,17 @@ func addExternalCandidates(ctx context.Context, pass *pass, refs references, fil
}()
for result := range results {
+ // Don't offer completions that would shadow predeclared
+ // names, such as github.com/coreos/etcd/error.
+ if types.Universe.Lookup(result.pkg.name) != nil { // predeclared
+ // Ideally we would skip this candidate only
+ // if the predeclared name is actually
+ // referenced by the file, but that's a lot
+ // trickier to compute and would still create
+ // an import that is likely to surprise the
+ // user before long.
+ continue
+ }
pass.addCandidate(result.imp, result.pkg)
}
return firstErr
@@ -1193,31 +1262,22 @@ func ImportPathToAssumedName(importPath string) string {
type gopathResolver struct {
env *ProcessEnv
walked bool
- cache *dirInfoCache
+ cache *DirInfoCache
scanSema chan struct{} // scanSema prevents concurrent scans.
}
func newGopathResolver(env *ProcessEnv) *gopathResolver {
r := &gopathResolver{
- env: env,
- cache: &dirInfoCache{
- dirs: map[string]*directoryPackageInfo{},
- listeners: map[*int]cacheListener{},
- },
+ env: env,
+ cache: NewDirInfoCache(),
scanSema: make(chan struct{}, 1),
}
r.scanSema <- struct{}{}
return r
}
-func (r *gopathResolver) ClearForNewScan() {
- <-r.scanSema
- r.cache = &dirInfoCache{
- dirs: map[string]*directoryPackageInfo{},
- listeners: map[*int]cacheListener{},
- }
- r.walked = false
- r.scanSema <- struct{}{}
+func (r *gopathResolver) ClearForNewScan() Resolver {
+ return newGopathResolver(r.env)
}
func (r *gopathResolver) loadPackageNames(importPaths []string, srcDir string) (map[string]string, error) {
@@ -1538,7 +1598,7 @@ func loadExportsFromFiles(ctx context.Context, env *ProcessEnv, dir string, incl
// findImport searches for a package with the given symbols.
// If no package is found, findImport returns ("", false, nil)
-func findImport(ctx context.Context, pass *pass, candidates []pkgDistance, pkgName string, symbols map[string]bool, filename string) (*pkg, error) {
+func findImport(ctx context.Context, pass *pass, candidates []pkgDistance, pkgName string, symbols map[string]bool) (*pkg, error) {
// Sort the candidates by their import package length,
// assuming that shorter package names are better than long
// ones. Note that this sorts by the de-vendored name, so
diff --git a/internal/imports/imports.go b/internal/imports/imports.go
index 58e637b90f2..660407548e5 100644
--- a/internal/imports/imports.go
+++ b/internal/imports/imports.go
@@ -236,7 +236,7 @@ func parse(fset *token.FileSet, filename string, src []byte, opt *Options) (*ast
src = src[:len(src)-len("}\n")]
// Gofmt has also indented the function body one level.
// Remove that indent.
- src = bytes.Replace(src, []byte("\n\t"), []byte("\n"), -1)
+ src = bytes.ReplaceAll(src, []byte("\n\t"), []byte("\n"))
return matchSpace(orig, src)
}
return file, adjust, nil
diff --git a/internal/imports/mod.go b/internal/imports/mod.go
index 5f4d435d3cc..3d0f38f6c23 100644
--- a/internal/imports/mod.go
+++ b/internal/imports/mod.go
@@ -23,49 +23,88 @@ import (
"golang.org/x/tools/internal/gopathwalk"
)
-// ModuleResolver implements resolver for modules using the go command as little
-// as feasible.
+// Notes(rfindley): ModuleResolver appears to be heavily optimized for scanning
+// as fast as possible, which is desirable for a call to goimports from the
+// command line, but it doesn't work as well for gopls, where it suffers from
+// slow startup (golang/go#44863) and intermittent hanging (golang/go#59216),
+// both caused by populating the cache, albeit in slightly different ways.
+//
+// A high level list of TODOs:
+// - Optimize the scan itself, as there is some redundancy statting and
+// reading go.mod files.
+// - Invert the relationship between ProcessEnv and Resolver (see the
+// docstring of ProcessEnv).
+// - Make it easier to use an external resolver implementation.
+//
+// Smaller TODOs are annotated in the code below.
+
+// ModuleResolver implements the Resolver interface for a workspace using
+// modules.
+//
+// A goal of the ModuleResolver is to invoke the Go command as little as
+// possible. To this end, it runs the Go command only for listing module
+// information (i.e. `go list -m -e -json ...`). Package scanning, the process
+// of loading package information for the modules, is implemented internally
+// via the scan method.
+//
+// It has two types of state: the state derived from the go command, which
+// is populated by init, and the state derived from scans, which is populated
+// via scan. A root is considered scanned if it has been walked to discover
+// directories. However, if the scan did not require additional information
+// from the directory (such as package name or exports), the directory
+// information itself may be partially populated. It will be lazily filled in
+// as needed by scans, using the scanCallback.
type ModuleResolver struct {
- env *ProcessEnv
- moduleCacheDir string
- dummyVendorMod *gocommand.ModuleJSON // If vendoring is enabled, the pseudo-module that represents the /vendor directory.
- roots []gopathwalk.Root
- scanSema chan struct{} // scanSema prevents concurrent scans and guards scannedRoots.
- scannedRoots map[gopathwalk.Root]bool
-
- initialized bool
- mains []*gocommand.ModuleJSON
- mainByDir map[string]*gocommand.ModuleJSON
- modsByModPath []*gocommand.ModuleJSON // All modules, ordered by # of path components in module Path...
- modsByDir []*gocommand.ModuleJSON // ...or number of path components in their Dir.
-
- // moduleCacheCache stores information about the module cache.
- moduleCacheCache *dirInfoCache
- otherCache *dirInfoCache
+ env *ProcessEnv
+
+ // Module state, populated during construction
+ dummyVendorMod *gocommand.ModuleJSON // if vendoring is enabled, a pseudo-module to represent the /vendor directory
+ moduleCacheDir string // GOMODCACHE, inferred from GOPATH if unset
+ roots []gopathwalk.Root // roots to scan, in approximate order of importance
+ mains []*gocommand.ModuleJSON // main modules
+ mainByDir map[string]*gocommand.ModuleJSON // module information by dir, to join with roots
+ modsByModPath []*gocommand.ModuleJSON // all modules, ordered by # of path components in their module path
+ modsByDir []*gocommand.ModuleJSON // ...or by the number of path components in their Dir.
+
+ // Scanning state, populated by scan
+
+ // scanSema prevents concurrent scans, and guards scannedRoots and the cache
+ // fields below (though the caches themselves are concurrency safe).
+ // Receive to acquire, send to release.
+ scanSema chan struct{}
+ scannedRoots map[gopathwalk.Root]bool // if true, root has been walked
+
+ // Caches of directory info, populated by scans and scan callbacks
+ //
+ // moduleCacheCache stores cached information about roots in the module
+ // cache, which are immutable and therefore do not need to be invalidated.
+ //
+ // otherCache stores information about all other roots (even GOROOT), which
+ // may change.
+ moduleCacheCache *DirInfoCache
+ otherCache *DirInfoCache
}
-func newModuleResolver(e *ProcessEnv) *ModuleResolver {
+// newModuleResolver returns a new module-aware goimports resolver.
+//
+// Note: use caution when modifying this constructor: changes must also be
+// reflected in ModuleResolver.ClearForNewScan.
+func newModuleResolver(e *ProcessEnv, moduleCacheCache *DirInfoCache) (*ModuleResolver, error) {
r := &ModuleResolver{
env: e,
scanSema: make(chan struct{}, 1),
}
- r.scanSema <- struct{}{}
- return r
-}
-
-func (r *ModuleResolver) init() error {
- if r.initialized {
- return nil
- }
+ r.scanSema <- struct{}{} // release
goenv, err := r.env.goEnv()
if err != nil {
- return err
+ return nil, err
}
+
+ // TODO(rfindley): can we refactor to share logic with r.env.invokeGo?
inv := gocommand.Invocation{
BuildFlags: r.env.BuildFlags,
ModFlag: r.env.ModFlag,
- ModFile: r.env.ModFile,
Env: r.env.env(),
Logf: r.env.Logf,
WorkingDir: r.env.WorkingDir,
@@ -77,9 +116,12 @@ func (r *ModuleResolver) init() error {
// 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 {
+ // TODO(rfindley): VendorEnabled runs the go command to get GOFLAGS, but
+ // they should be available from the ProcessEnv. Can we avoid the redundant
+ // invocation?
vendorEnabled, mainModVendor, err = gocommand.VendorEnabled(context.TODO(), inv, r.env.GocmdRunner)
if err != nil {
- return err
+ return nil, err
}
}
@@ -100,19 +142,14 @@ func (r *ModuleResolver) init() error {
// GO111MODULE=on. Other errors are fatal.
if err != nil {
if errMsg := err.Error(); !strings.Contains(errMsg, "working directory is not part of a module") && !strings.Contains(errMsg, "go.mod file not found") {
- return err
+ return nil, err
}
}
}
- if gmc := r.env.Env["GOMODCACHE"]; gmc != "" {
- r.moduleCacheDir = gmc
- } else {
- gopaths := filepath.SplitList(goenv["GOPATH"])
- if len(gopaths) == 0 {
- return fmt.Errorf("empty GOPATH")
- }
- r.moduleCacheDir = filepath.Join(gopaths[0], "/pkg/mod")
+ r.moduleCacheDir = gomodcacheForEnv(goenv)
+ if r.moduleCacheDir == "" {
+ return nil, fmt.Errorf("cannot resolve GOMODCACHE")
}
sort.Slice(r.modsByModPath, func(i, j int) bool {
@@ -141,7 +178,11 @@ func (r *ModuleResolver) init() error {
} else {
addDep := func(mod *gocommand.ModuleJSON) {
if mod.Replace == nil {
- // This is redundant with the cache, but we'll skip it cheaply enough.
+ // This is redundant with the cache, but we'll skip it cheaply enough
+ // when we encounter it in the module cache scan.
+ //
+ // Including it at a lower index in r.roots than the module cache dir
+ // helps prioritize matches from within existing dependencies.
r.roots = append(r.roots, gopathwalk.Root{Path: mod.Dir, Type: gopathwalk.RootModuleCache})
} else {
r.roots = append(r.roots, gopathwalk.Root{Path: mod.Dir, Type: gopathwalk.RootOther})
@@ -158,24 +199,40 @@ func (r *ModuleResolver) init() error {
addDep(mod)
}
}
+ // If provided, share the moduleCacheCache.
+ //
+ // TODO(rfindley): The module cache is immutable. However, the loaded
+ // exports do depend on GOOS and GOARCH. Fortunately, the
+ // ProcessEnv.buildContext does not adjust these from build.DefaultContext
+ // (even though it should). So for now, this is OK to share, but we need to
+ // add logic for handling GOOS/GOARCH.
+ r.moduleCacheCache = moduleCacheCache
r.roots = append(r.roots, gopathwalk.Root{Path: r.moduleCacheDir, Type: gopathwalk.RootModuleCache})
}
r.scannedRoots = map[gopathwalk.Root]bool{}
if r.moduleCacheCache == nil {
- r.moduleCacheCache = &dirInfoCache{
- dirs: map[string]*directoryPackageInfo{},
- listeners: map[*int]cacheListener{},
- }
- }
- if r.otherCache == nil {
- r.otherCache = &dirInfoCache{
- dirs: map[string]*directoryPackageInfo{},
- listeners: map[*int]cacheListener{},
- }
+ r.moduleCacheCache = NewDirInfoCache()
}
- r.initialized = true
- return nil
+ r.otherCache = NewDirInfoCache()
+ return r, nil
+}
+
+// gomodcacheForEnv returns the GOMODCACHE value to use based on the given env
+// map, which must have GOMODCACHE and GOPATH populated.
+//
+// TODO(rfindley): this is defensive refactoring.
+// 1. Is this even relevant anymore? Can't we just read GOMODCACHE.
+// 2. Use this to separate module cache scanning from other scanning.
+func gomodcacheForEnv(goenv map[string]string) string {
+ if gmc := goenv["GOMODCACHE"]; gmc != "" {
+ return gmc
+ }
+ gopaths := filepath.SplitList(goenv["GOPATH"])
+ if len(gopaths) == 0 {
+ return ""
+ }
+ return filepath.Join(gopaths[0], "/pkg/mod")
}
func (r *ModuleResolver) initAllMods() error {
@@ -206,30 +263,82 @@ func (r *ModuleResolver) initAllMods() error {
return nil
}
-func (r *ModuleResolver) ClearForNewScan() {
- <-r.scanSema
- r.scannedRoots = map[gopathwalk.Root]bool{}
- r.otherCache = &dirInfoCache{
- dirs: map[string]*directoryPackageInfo{},
- listeners: map[*int]cacheListener{},
+// ClearForNewScan invalidates the last scan.
+//
+// It preserves the set of roots, but forgets about the set of directories.
+// Though it forgets the set of module cache directories, it remembers their
+// contents, since they are assumed to be immutable.
+func (r *ModuleResolver) ClearForNewScan() Resolver {
+ <-r.scanSema // acquire r, to guard scannedRoots
+ r2 := &ModuleResolver{
+ env: r.env,
+ dummyVendorMod: r.dummyVendorMod,
+ moduleCacheDir: r.moduleCacheDir,
+ roots: r.roots,
+ mains: r.mains,
+ mainByDir: r.mainByDir,
+ modsByModPath: r.modsByModPath,
+
+ scanSema: make(chan struct{}, 1),
+ scannedRoots: make(map[gopathwalk.Root]bool),
+ otherCache: NewDirInfoCache(),
+ moduleCacheCache: r.moduleCacheCache,
+ }
+ r2.scanSema <- struct{}{} // r2 must start released
+ // Invalidate root scans. We don't need to invalidate module cache roots,
+ // because they are immutable.
+ // (We don't support a use case where GOMODCACHE is cleaned in the middle of
+ // e.g. a gopls session: the user must restart gopls to get accurate
+ // imports.)
+ //
+ // Scanning for new directories in GOMODCACHE should be handled elsewhere,
+ // via a call to ScanModuleCache.
+ for _, root := range r.roots {
+ if root.Type == gopathwalk.RootModuleCache && r.scannedRoots[root] {
+ r2.scannedRoots[root] = true
+ }
}
- r.scanSema <- struct{}{}
+ r.scanSema <- struct{}{} // release r
+ return r2
}
-func (r *ModuleResolver) ClearForNewMod() {
- <-r.scanSema
- *r = ModuleResolver{
- env: r.env,
- moduleCacheCache: r.moduleCacheCache,
- otherCache: r.otherCache,
- scanSema: r.scanSema,
+// ClearModuleInfo invalidates resolver state that depends on go.mod file
+// contents (essentially, the output of go list -m -json ...).
+//
+// Notably, it does not forget directory contents, which are reset
+// asynchronously via ClearForNewScan.
+//
+// If the ProcessEnv is a GOPATH environment, ClearModuleInfo is a no op.
+//
+// TODO(rfindley): move this to a new env.go, consolidating ProcessEnv methods.
+func (e *ProcessEnv) ClearModuleInfo() {
+ if r, ok := e.resolver.(*ModuleResolver); ok {
+ resolver, resolverErr := newModuleResolver(e, e.ModCache)
+ if resolverErr == nil {
+ <-r.scanSema // acquire (guards caches)
+ resolver.moduleCacheCache = r.moduleCacheCache
+ resolver.otherCache = r.otherCache
+ r.scanSema <- struct{}{} // release
+ }
+ e.resolver = resolver
+ e.resolverErr = resolverErr
}
- r.init()
- r.scanSema <- struct{}{}
}
-// findPackage returns the module and directory that contains the package at
-// the given import path, or returns nil, "" if no module is in scope.
+// UpdateResolver sets the resolver for the ProcessEnv to use in imports
+// operations. Only for use with the result of [Resolver.ClearForNewScan].
+//
+// TODO(rfindley): this awkward API is a result of the (arguably) inverted
+// relationship between configuration and state described in the doc comment
+// for [ProcessEnv].
+func (e *ProcessEnv) UpdateResolver(r Resolver) {
+ e.resolver = r
+ e.resolverErr = nil
+}
+
+// findPackage returns the module and directory from within the main modules
+// and their dependencies that contains the package at the given import path,
+// or returns nil, "" if no module is in scope.
func (r *ModuleResolver) findPackage(importPath string) (*gocommand.ModuleJSON, string) {
// This can't find packages in the stdlib, but that's harmless for all
// the existing code paths.
@@ -295,10 +404,6 @@ func (r *ModuleResolver) cacheStore(info directoryPackageInfo) {
}
}
-func (r *ModuleResolver) cacheKeys() []string {
- return append(r.moduleCacheCache.Keys(), r.otherCache.Keys()...)
-}
-
// cachePackageName caches the package name for a dir already in the cache.
func (r *ModuleResolver) cachePackageName(info directoryPackageInfo) (string, error) {
if info.rootType == gopathwalk.RootModuleCache {
@@ -367,15 +472,15 @@ func (r *ModuleResolver) dirIsNestedModule(dir string, mod *gocommand.ModuleJSON
return modDir != mod.Dir
}
-func (r *ModuleResolver) modInfo(dir string) (modDir string, modName string) {
- readModName := func(modFile string) string {
- modBytes, err := os.ReadFile(modFile)
- if err != nil {
- return ""
- }
- return modulePath(modBytes)
+func readModName(modFile string) string {
+ modBytes, err := os.ReadFile(modFile)
+ if err != nil {
+ return ""
}
+ return modulePath(modBytes)
+}
+func (r *ModuleResolver) modInfo(dir string) (modDir, modName string) {
if r.dirInModuleCache(dir) {
if matches := modCacheRegexp.FindStringSubmatch(dir); len(matches) == 3 {
index := strings.Index(dir, matches[1]+"@"+matches[2])
@@ -409,11 +514,9 @@ func (r *ModuleResolver) dirInModuleCache(dir string) bool {
}
func (r *ModuleResolver) loadPackageNames(importPaths []string, srcDir string) (map[string]string, error) {
- if err := r.init(); err != nil {
- return nil, err
- }
names := map[string]string{}
for _, path := range importPaths {
+ // TODO(rfindley): shouldn't this use the dirInfoCache?
_, packageDir := r.findPackage(path)
if packageDir == "" {
continue
@@ -431,10 +534,6 @@ func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error
ctx, done := event.Start(ctx, "imports.ModuleResolver.scan")
defer done()
- if err := r.init(); err != nil {
- return err
- }
-
processDir := func(info directoryPackageInfo) {
// Skip this directory if we were not able to get the package information successfully.
if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil {
@@ -444,18 +543,18 @@ func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error
if err != nil {
return
}
-
if !callback.dirFound(pkg) {
return
}
+
pkg.packageName, err = r.cachePackageName(info)
if err != nil {
return
}
-
if !callback.packageNameLoaded(pkg) {
return
}
+
_, exports, err := r.loadExports(ctx, pkg, false)
if err != nil {
return
@@ -494,7 +593,6 @@ func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error
return packageScanned
}
- // Add anything new to the cache, and process it if we're still listening.
add := func(root gopathwalk.Root, dir string) {
r.cacheStore(r.scanDirForPackage(root, dir))
}
@@ -509,9 +607,9 @@ func (r *ModuleResolver) scan(ctx context.Context, callback *scanCallback) error
select {
case <-ctx.Done():
return
- case <-r.scanSema:
+ case <-r.scanSema: // acquire
}
- defer func() { r.scanSema <- struct{}{} }()
+ defer func() { r.scanSema <- struct{}{} }() // release
// We have the lock on r.scannedRoots, and no other scans can run.
for _, root := range roots {
if ctx.Err() != nil {
@@ -613,9 +711,6 @@ func (r *ModuleResolver) canonicalize(info directoryPackageInfo) (*pkg, error) {
}
func (r *ModuleResolver) loadExports(ctx context.Context, pkg *pkg, includeTest bool) (string, []string, error) {
- if err := r.init(); err != nil {
- return "", nil, err
- }
if info, ok := r.cacheLoad(pkg.dir); ok && !includeTest {
return r.cacheExports(ctx, r.env, info)
}
diff --git a/internal/imports/mod_cache.go b/internal/imports/mod_cache.go
index 45690abbb4f..cfc54657656 100644
--- a/internal/imports/mod_cache.go
+++ b/internal/imports/mod_cache.go
@@ -7,8 +7,12 @@ package imports
import (
"context"
"fmt"
+ "path"
+ "path/filepath"
+ "strings"
"sync"
+ "golang.org/x/mod/module"
"golang.org/x/tools/internal/gopathwalk"
)
@@ -39,6 +43,8 @@ const (
exportsLoaded
)
+// directoryPackageInfo holds (possibly incomplete) information about packages
+// contained in a given directory.
type directoryPackageInfo struct {
// status indicates the extent to which this struct has been filled in.
status directoryPackageStatus
@@ -63,7 +69,10 @@ type directoryPackageInfo struct {
packageName string // the package name, as declared in the source.
// Set when status >= exportsLoaded.
-
+ // TODO(rfindley): it's hard to see this, but exports depend implicitly on
+ // the default build context GOOS and GOARCH.
+ //
+ // We can make this explicit, and key exports by GOOS, GOARCH.
exports []string
}
@@ -79,7 +88,7 @@ func (info *directoryPackageInfo) reachedStatus(target directoryPackageStatus) (
return true, nil
}
-// dirInfoCache is a concurrency safe map for storing information about
+// DirInfoCache is a concurrency-safe map for storing information about
// directories that may contain packages.
//
// The information in this cache is built incrementally. Entries are initialized in scan.
@@ -92,21 +101,26 @@ func (info *directoryPackageInfo) reachedStatus(target directoryPackageStatus) (
// The information in the cache is not expected to change for the cache's
// lifetime, so there is no protection against competing writes. Users should
// take care not to hold the cache across changes to the underlying files.
-//
-// TODO(suzmue): consider other concurrency strategies and data structures (RWLocks, sync.Map, etc)
-type dirInfoCache struct {
+type DirInfoCache struct {
mu sync.Mutex
// dirs stores information about packages in directories, keyed by absolute path.
dirs map[string]*directoryPackageInfo
listeners map[*int]cacheListener
}
+func NewDirInfoCache() *DirInfoCache {
+ return &DirInfoCache{
+ dirs: make(map[string]*directoryPackageInfo),
+ listeners: make(map[*int]cacheListener),
+ }
+}
+
type cacheListener func(directoryPackageInfo)
// ScanAndListen calls listener on all the items in the cache, and on anything
// newly added. The returned stop function waits for all in-flight callbacks to
// finish and blocks new ones.
-func (d *dirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener) func() {
+func (d *DirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener) func() {
ctx, cancel := context.WithCancel(ctx)
// Flushing out all the callbacks is tricky without knowing how many there
@@ -162,8 +176,10 @@ func (d *dirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener
}
// Store stores the package info for dir.
-func (d *dirInfoCache) Store(dir string, info directoryPackageInfo) {
+func (d *DirInfoCache) Store(dir string, info directoryPackageInfo) {
d.mu.Lock()
+ // TODO(rfindley, golang/go#59216): should we overwrite an existing entry?
+ // That seems incorrect as the cache should be idempotent.
_, old := d.dirs[dir]
d.dirs[dir] = &info
var listeners []cacheListener
@@ -180,7 +196,7 @@ func (d *dirInfoCache) Store(dir string, info directoryPackageInfo) {
}
// Load returns a copy of the directoryPackageInfo for absolute directory dir.
-func (d *dirInfoCache) Load(dir string) (directoryPackageInfo, bool) {
+func (d *DirInfoCache) Load(dir string) (directoryPackageInfo, bool) {
d.mu.Lock()
defer d.mu.Unlock()
info, ok := d.dirs[dir]
@@ -191,7 +207,7 @@ func (d *dirInfoCache) Load(dir string) (directoryPackageInfo, bool) {
}
// Keys returns the keys currently present in d.
-func (d *dirInfoCache) Keys() (keys []string) {
+func (d *DirInfoCache) Keys() (keys []string) {
d.mu.Lock()
defer d.mu.Unlock()
for key := range d.dirs {
@@ -200,7 +216,7 @@ func (d *dirInfoCache) Keys() (keys []string) {
return keys
}
-func (d *dirInfoCache) CachePackageName(info directoryPackageInfo) (string, error) {
+func (d *DirInfoCache) CachePackageName(info directoryPackageInfo) (string, error) {
if loaded, err := info.reachedStatus(nameLoaded); loaded {
return info.packageName, err
}
@@ -213,7 +229,7 @@ func (d *dirInfoCache) CachePackageName(info directoryPackageInfo) (string, erro
return info.packageName, info.err
}
-func (d *dirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []string, error) {
+func (d *DirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []string, error) {
if reached, _ := info.reachedStatus(exportsLoaded); reached {
return info.packageName, info.exports, info.err
}
@@ -234,3 +250,81 @@ func (d *dirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info d
d.Store(info.dir, info)
return info.packageName, info.exports, info.err
}
+
+// ScanModuleCache walks the given directory, which must be a GOMODCACHE value,
+// for directory package information, storing the results in cache.
+func ScanModuleCache(dir string, cache *DirInfoCache, logf func(string, ...any)) {
+ // Note(rfindley): it's hard to see, but this function attempts to implement
+ // just the side effects on cache of calling PrimeCache with a ProcessEnv
+ // that has the given dir as its GOMODCACHE.
+ //
+ // Teasing out the control flow, we see that we can avoid any handling of
+ // vendor/ and can infer module info entirely from the path, simplifying the
+ // logic here.
+
+ root := gopathwalk.Root{
+ Path: filepath.Clean(dir),
+ Type: gopathwalk.RootModuleCache,
+ }
+
+ directoryInfo := func(root gopathwalk.Root, dir string) directoryPackageInfo {
+ // This is a copy of ModuleResolver.scanDirForPackage, trimmed down to
+ // logic that applies to a module cache directory.
+
+ subdir := ""
+ if dir != root.Path {
+ subdir = dir[len(root.Path)+len("/"):]
+ }
+
+ matches := modCacheRegexp.FindStringSubmatch(subdir)
+ if len(matches) == 0 {
+ return directoryPackageInfo{
+ status: directoryScanned,
+ err: fmt.Errorf("invalid module cache path: %v", subdir),
+ }
+ }
+ modPath, err := module.UnescapePath(filepath.ToSlash(matches[1]))
+ if err != nil {
+ if logf != nil {
+ logf("decoding module cache path %q: %v", subdir, err)
+ }
+ return directoryPackageInfo{
+ status: directoryScanned,
+ err: fmt.Errorf("decoding module cache path %q: %v", subdir, err),
+ }
+ }
+ importPath := path.Join(modPath, filepath.ToSlash(matches[3]))
+ index := strings.Index(dir, matches[1]+"@"+matches[2])
+ modDir := filepath.Join(dir[:index], matches[1]+"@"+matches[2])
+ modName := readModName(filepath.Join(modDir, "go.mod"))
+ return directoryPackageInfo{
+ status: directoryScanned,
+ dir: dir,
+ rootType: root.Type,
+ nonCanonicalImportPath: importPath,
+ moduleDir: modDir,
+ moduleName: modName,
+ }
+ }
+
+ add := func(root gopathwalk.Root, dir string) {
+ info := directoryInfo(root, dir)
+ cache.Store(info.dir, info)
+ }
+
+ skip := func(_ gopathwalk.Root, dir string) bool {
+ // Skip directories that have already been scanned.
+ //
+ // Note that gopathwalk only adds "package" directories, which must contain
+ // a .go file, and all such package directories in the module cache are
+ // immutable. So if we can load a dir, it can be skipped.
+ info, ok := cache.Load(dir)
+ if !ok {
+ return false
+ }
+ packageScanned, _ := info.reachedStatus(directoryScanned)
+ return packageScanned
+ }
+
+ gopathwalk.WalkSkip([]gopathwalk.Root{root}, add, skip, gopathwalk.Options{Logf: logf, ModulesEnabled: true})
+}
diff --git a/internal/imports/mod_cache_test.go b/internal/imports/mod_cache_test.go
index 39c691e5330..3af85fb7f56 100644
--- a/internal/imports/mod_cache_test.go
+++ b/internal/imports/mod_cache_test.go
@@ -6,9 +6,12 @@ package imports
import (
"fmt"
+ "os/exec"
"reflect"
"sort"
+ "strings"
"testing"
+ "time"
)
func TestDirectoryPackageInfoReachedStatus(t *testing.T) {
@@ -58,9 +61,7 @@ func TestDirectoryPackageInfoReachedStatus(t *testing.T) {
}
func TestModCacheInfo(t *testing.T) {
- m := &dirInfoCache{
- dirs: make(map[string]*directoryPackageInfo),
- }
+ m := NewDirInfoCache()
dirInfo := []struct {
dir string
@@ -124,3 +125,20 @@ func TestModCacheInfo(t *testing.T) {
}
}
}
+
+func BenchmarkScanModuleCache(b *testing.B) {
+ output, err := exec.Command("go", "env", "GOMODCACHE").Output()
+ if err != nil {
+ b.Fatal(err)
+ }
+ gomodcache := strings.TrimSpace(string(output))
+ cache := NewDirInfoCache()
+ start := time.Now()
+ ScanModuleCache(gomodcache, cache, nil)
+ b.Logf("initial scan took %v", time.Since(start))
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ ScanModuleCache(gomodcache, cache, nil)
+ }
+}
diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go
index 26dac639062..c624463895d 100644
--- a/internal/imports/mod_test.go
+++ b/internal/imports/mod_test.go
@@ -17,6 +17,7 @@ import (
"strings"
"sync"
"testing"
+ "time"
"golang.org/x/mod/module"
"golang.org/x/tools/internal/gocommand"
@@ -93,7 +94,7 @@ package z
mt.assertFound("y", "y")
- scan, err := scanToSlice(mt.resolver, nil)
+ scan, err := scanToSlice(mt.env.resolver, nil)
if err != nil {
t.Fatal(err)
}
@@ -212,7 +213,7 @@ import _ "rsc.io/quote"
}
// Uninitialize the go.mod dependent cached information and make sure it still finds the package.
- mt.resolver.ClearForNewMod()
+ mt.env.ClearModuleInfo()
mt.assertScanFinds("rsc.io/quote", "quote")
}
@@ -241,8 +242,9 @@ import _ "rsc.io/sampler"
}
// Clear out the resolver's cache, since we've changed the environment.
- mt.resolver = newModuleResolver(mt.env)
mt.env.Env["GOFLAGS"] = "-mod=vendor"
+ mt.env.ClearModuleInfo()
+ mt.env.UpdateResolver(mt.env.resolver.ClearForNewScan())
mt.assertModuleFoundInDir("rsc.io/sampler", "sampler", `/vendor/`)
}
@@ -269,6 +271,10 @@ import _ "rsc.io/sampler"
if testenv.Go1Point() >= 14 {
wantDir = `/vendor/`
}
+
+ // Clear out the resolver's module info, since we've changed the environment.
+ // (the presence of a /vendor directory affects `go list -m`).
+ mt.env.ClearModuleInfo()
mt.assertModuleFoundInDir("rsc.io/sampler", "sampler", wantDir)
}
@@ -900,7 +906,7 @@ package x
func (t *modTest) assertFound(importPath, pkgName string) (string, *pkg) {
t.Helper()
- names, err := t.resolver.loadPackageNames([]string{importPath}, t.env.WorkingDir)
+ names, err := t.env.resolver.loadPackageNames([]string{importPath}, t.env.WorkingDir)
if err != nil {
t.Errorf("loading package name for %v: %v", importPath, err)
}
@@ -909,13 +915,13 @@ func (t *modTest) assertFound(importPath, pkgName string) (string, *pkg) {
}
pkg := t.assertScanFinds(importPath, pkgName)
- _, foundDir := t.resolver.findPackage(importPath)
+ _, foundDir := t.env.resolver.(*ModuleResolver).findPackage(importPath)
return foundDir, pkg
}
func (t *modTest) assertScanFinds(importPath, pkgName string) *pkg {
t.Helper()
- scan, err := scanToSlice(t.resolver, nil)
+ scan, err := scanToSlice(t.env.resolver, nil)
if err != nil {
t.Errorf("scan failed: %v", err)
}
@@ -983,10 +989,9 @@ var proxyDir string
type modTest struct {
*testing.T
- env *ProcessEnv
- gopath string
- resolver *ModuleResolver
- cleanup func()
+ env *ProcessEnv
+ gopath string
+ cleanup func()
}
// setup builds a test environment from a txtar and supporting modules
@@ -1046,16 +1051,20 @@ func setup(t *testing.T, extraEnv map[string]string, main, wd string) *modTest {
}
}
- resolver, err := env.GetResolver()
- if err != nil {
+ // Ensure the resolver is set for tests that (unsafely) access env.resolver
+ // directly.
+ //
+ // TODO(rfindley): fix this after addressing the TODO in the ProcessEnv
+ // docstring.
+ if _, err := env.GetResolver(); err != nil {
t.Fatal(err)
}
+
return &modTest{
- T: t,
- gopath: env.Env["GOPATH"],
- env: env,
- resolver: resolver.(*ModuleResolver),
- cleanup: func() { removeDir(dir) },
+ T: t,
+ gopath: env.Env["GOPATH"],
+ env: env,
+ cleanup: func() { removeDir(dir) },
}
}
@@ -1098,7 +1107,7 @@ func writeProxyModule(base, arPath string) error {
arName := filepath.Base(arPath)
i := strings.LastIndex(arName, "_v")
ver := strings.TrimSuffix(arName[i+1:], ".txt")
- modDir := strings.Replace(arName[:i], "_", "/", -1)
+ modDir := strings.ReplaceAll(arName[:i], "_", "/")
modPath, err := module.UnescapePath(modDir)
if err != nil {
return err
@@ -1183,7 +1192,7 @@ import _ "rsc.io/quote"
want := filepath.Join(mt.gopath, "pkg/mod", "rsc.io/quote@v1.5.2")
found := mt.assertScanFinds("rsc.io/quote", "quote")
- modDir, _ := mt.resolver.modInfo(found.dir)
+ modDir, _ := mt.env.resolver.(*ModuleResolver).modInfo(found.dir)
if modDir != want {
t.Errorf("expected: %s, got: %s", want, modDir)
}
@@ -1288,20 +1297,37 @@ import (
}
}
-func BenchmarkScanModCache(b *testing.B) {
+func BenchmarkModuleResolver_RescanModCache(b *testing.B) {
env := &ProcessEnv{
GocmdRunner: &gocommand.Runner{},
- Logf: b.Logf,
+ // Uncomment for verbose logging (too verbose to enable by default).
+ // Logf: b.Logf,
}
exclude := []gopathwalk.RootType{gopathwalk.RootGOROOT}
resolver, err := env.GetResolver()
if err != nil {
b.Fatal(err)
}
+ start := time.Now()
scanToSlice(resolver, exclude)
+ b.Logf("warming the mod cache took %v", time.Since(start))
b.ResetTimer()
for i := 0; i < b.N; i++ {
scanToSlice(resolver, exclude)
- resolver.(*ModuleResolver).ClearForNewScan()
+ resolver = resolver.ClearForNewScan()
+ }
+}
+
+func BenchmarkModuleResolver_InitialScan(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ env := &ProcessEnv{
+ GocmdRunner: &gocommand.Runner{},
+ }
+ exclude := []gopathwalk.RootType{gopathwalk.RootGOROOT}
+ resolver, err := env.GetResolver()
+ if err != nil {
+ b.Fatal(err)
+ }
+ scanToSlice(resolver, exclude)
}
}
diff --git a/internal/imports/zstdlib.go b/internal/imports/zstdlib.go
index 9f992c2bec8..8db24df2ff4 100644
--- a/internal/imports/zstdlib.go
+++ b/internal/imports/zstdlib.go
@@ -151,6 +151,7 @@ var stdlib = map[string][]string{
"cmp": {
"Compare",
"Less",
+ "Or",
"Ordered",
},
"compress/bzip2": {
@@ -632,6 +633,8 @@ var stdlib = map[string][]string{
"NameMismatch",
"NewCertPool",
"NotAuthorizedToSign",
+ "OID",
+ "OIDFromInts",
"PEMCipher",
"PEMCipher3DES",
"PEMCipherAES128",
@@ -706,6 +709,7 @@ var stdlib = map[string][]string{
"LevelWriteCommitted",
"Named",
"NamedArg",
+ "Null",
"NullBool",
"NullByte",
"NullFloat64",
@@ -1921,6 +1925,7 @@ var stdlib = map[string][]string{
"R_LARCH_32",
"R_LARCH_32_PCREL",
"R_LARCH_64",
+ "R_LARCH_64_PCREL",
"R_LARCH_ABS64_HI12",
"R_LARCH_ABS64_LO20",
"R_LARCH_ABS_HI20",
@@ -1928,12 +1933,17 @@ var stdlib = map[string][]string{
"R_LARCH_ADD16",
"R_LARCH_ADD24",
"R_LARCH_ADD32",
+ "R_LARCH_ADD6",
"R_LARCH_ADD64",
"R_LARCH_ADD8",
+ "R_LARCH_ADD_ULEB128",
+ "R_LARCH_ALIGN",
"R_LARCH_B16",
"R_LARCH_B21",
"R_LARCH_B26",
+ "R_LARCH_CFA",
"R_LARCH_COPY",
+ "R_LARCH_DELETE",
"R_LARCH_GNU_VTENTRY",
"R_LARCH_GNU_VTINHERIT",
"R_LARCH_GOT64_HI12",
@@ -1953,6 +1963,7 @@ var stdlib = map[string][]string{
"R_LARCH_PCALA64_LO20",
"R_LARCH_PCALA_HI20",
"R_LARCH_PCALA_LO12",
+ "R_LARCH_PCREL20_S2",
"R_LARCH_RELATIVE",
"R_LARCH_RELAX",
"R_LARCH_SOP_ADD",
@@ -1983,8 +1994,10 @@ var stdlib = map[string][]string{
"R_LARCH_SUB16",
"R_LARCH_SUB24",
"R_LARCH_SUB32",
+ "R_LARCH_SUB6",
"R_LARCH_SUB64",
"R_LARCH_SUB8",
+ "R_LARCH_SUB_ULEB128",
"R_LARCH_TLS_DTPMOD32",
"R_LARCH_TLS_DTPMOD64",
"R_LARCH_TLS_DTPREL32",
@@ -2035,6 +2048,7 @@ var stdlib = map[string][]string{
"R_MIPS_LO16",
"R_MIPS_NONE",
"R_MIPS_PC16",
+ "R_MIPS_PC32",
"R_MIPS_PJUMP",
"R_MIPS_REL16",
"R_MIPS_REL32",
@@ -2952,6 +2966,8 @@ var stdlib = map[string][]string{
"RegisterName",
},
"encoding/hex": {
+ "AppendDecode",
+ "AppendEncode",
"Decode",
"DecodeString",
"DecodedLen",
@@ -3233,6 +3249,7 @@ var stdlib = map[string][]string{
"TypeSpec",
"TypeSwitchStmt",
"UnaryExpr",
+ "Unparen",
"ValueSpec",
"Var",
"Visitor",
@@ -3492,6 +3509,7 @@ var stdlib = map[string][]string{
"XOR_ASSIGN",
},
"go/types": {
+ "Alias",
"ArgumentError",
"Array",
"AssertableTo",
@@ -3559,6 +3577,7 @@ var stdlib = map[string][]string{
"MethodVal",
"MissingMethod",
"Named",
+ "NewAlias",
"NewArray",
"NewChan",
"NewChecker",
@@ -3627,6 +3646,7 @@ var stdlib = map[string][]string{
"Uint64",
"Uint8",
"Uintptr",
+ "Unalias",
"Union",
"Universe",
"Unsafe",
@@ -3643,6 +3663,11 @@ var stdlib = map[string][]string{
"WriteSignature",
"WriteType",
},
+ "go/version": {
+ "Compare",
+ "IsValid",
+ "Lang",
+ },
"hash": {
"Hash",
"Hash32",
@@ -4078,6 +4103,7 @@ var stdlib = map[string][]string{
"NewTextHandler",
"Record",
"SetDefault",
+ "SetLogLoggerLevel",
"Source",
"SourceKey",
"String",
@@ -4367,6 +4393,35 @@ var stdlib = map[string][]string{
"Uint64",
"Zipf",
},
+ "math/rand/v2": {
+ "ChaCha8",
+ "ExpFloat64",
+ "Float32",
+ "Float64",
+ "Int",
+ "Int32",
+ "Int32N",
+ "Int64",
+ "Int64N",
+ "IntN",
+ "N",
+ "New",
+ "NewChaCha8",
+ "NewPCG",
+ "NewZipf",
+ "NormFloat64",
+ "PCG",
+ "Perm",
+ "Rand",
+ "Shuffle",
+ "Source",
+ "Uint32",
+ "Uint32N",
+ "Uint64",
+ "Uint64N",
+ "UintN",
+ "Zipf",
+ },
"mime": {
"AddExtensionType",
"BEncoding",
@@ -4540,6 +4595,7 @@ var stdlib = map[string][]string{
"FS",
"File",
"FileServer",
+ "FileServerFS",
"FileSystem",
"Flusher",
"Get",
@@ -4566,6 +4622,7 @@ var stdlib = map[string][]string{
"MethodPut",
"MethodTrace",
"NewFileTransport",
+ "NewFileTransportFS",
"NewRequest",
"NewRequestWithContext",
"NewResponseController",
@@ -4599,6 +4656,7 @@ var stdlib = map[string][]string{
"Serve",
"ServeContent",
"ServeFile",
+ "ServeFileFS",
"ServeMux",
"ServeTLS",
"Server",
@@ -5106,6 +5164,7 @@ var stdlib = map[string][]string{
"StructTag",
"Swapper",
"Type",
+ "TypeFor",
"TypeOf",
"Uint",
"Uint16",
@@ -5342,6 +5401,7 @@ var stdlib = map[string][]string{
"CompactFunc",
"Compare",
"CompareFunc",
+ "Concat",
"Contains",
"ContainsFunc",
"Delete",
@@ -10824,6 +10884,7 @@ var stdlib = map[string][]string{
"Value",
},
"testing/slogtest": {
+ "Run",
"TestHandler",
},
"text/scanner": {
diff --git a/internal/jsonrpc2/messages.go b/internal/jsonrpc2/messages.go
index 9ff47f3d1d5..721168fd4f2 100644
--- a/internal/jsonrpc2/messages.go
+++ b/internal/jsonrpc2/messages.go
@@ -157,17 +157,17 @@ func (r *Response) MarshalJSON() ([]byte, error) {
return data, nil
}
-func toWireError(err error) *wireError {
+func toWireError(err error) *WireError {
if err == nil {
// no error, the response is complete
return nil
}
- if err, ok := err.(*wireError); ok {
+ if err, ok := err.(*WireError); ok {
// already a wire error, just use it
return err
}
- result := &wireError{Message: err.Error()}
- var wrapped *wireError
+ result := &WireError{Message: err.Error()}
+ var wrapped *WireError
if errors.As(err, &wrapped) {
// if we wrapped a wire error, keep the code from the wrapped error
// but the message from the outer error
diff --git a/internal/jsonrpc2/wire.go b/internal/jsonrpc2/wire.go
index ac39f1601f0..f2aa2d63e8c 100644
--- a/internal/jsonrpc2/wire.go
+++ b/internal/jsonrpc2/wire.go
@@ -57,7 +57,7 @@ type wireResponse struct {
// Result is the response value, and is required on success.
Result *json.RawMessage `json:"result,omitempty"`
// Error is a structured error response if the call fails.
- Error *wireError `json:"error,omitempty"`
+ Error *WireError `json:"error,omitempty"`
// ID must be set and is the identifier of the Request this is a response to.
ID *ID `json:"id,omitempty"`
}
@@ -70,11 +70,11 @@ type wireCombined struct {
Method string `json:"method"`
Params *json.RawMessage `json:"params,omitempty"`
Result *json.RawMessage `json:"result,omitempty"`
- Error *wireError `json:"error,omitempty"`
+ Error *WireError `json:"error,omitempty"`
}
-// wireError represents a structured error in a Response.
-type wireError struct {
+// WireError represents a structured error in a Response.
+type WireError struct {
// Code is an error code indicating the type of failure.
Code int64 `json:"code"`
// Message is a short description of the error.
@@ -96,13 +96,13 @@ type ID struct {
}
func NewError(code int64, message string) error {
- return &wireError{
+ return &WireError{
Code: code,
Message: message,
}
}
-func (err *wireError) Error() string {
+func (err *WireError) Error() string {
return err.Message
}
diff --git a/internal/refactor/inline/escape.go b/internal/refactor/inline/escape.go
index 795ad4feab6..a3f5e555e9f 100644
--- a/internal/refactor/inline/escape.go
+++ b/internal/refactor/inline/escape.go
@@ -72,7 +72,7 @@ func escape(info *types.Info, root ast.Node, f func(v *types.Var, escapes bool))
if sel, ok := n.Fun.(*ast.SelectorExpr); ok {
if seln, ok := info.Selections[sel]; ok &&
seln.Kind() == types.MethodVal &&
- isPointer(seln.Obj().Type().(*types.Signature).Recv().Type()) {
+ isPointer(seln.Obj().Type().Underlying().(*types.Signature).Recv().Type()) {
tArg, indirect := effectiveReceiver(seln)
if !indirect && !isPointer(tArg) {
lvalue(sel.X, true) // &x.f
diff --git a/internal/refactor/inline/falcon.go b/internal/refactor/inline/falcon.go
index 9863e8dbcfb..1b2cb746ade 100644
--- a/internal/refactor/inline/falcon.go
+++ b/internal/refactor/inline/falcon.go
@@ -17,6 +17,7 @@ import (
"strings"
"golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/aliases"
"golang.org/x/tools/internal/typeparams"
)
@@ -446,7 +447,7 @@ func (st *falconState) expr(e ast.Expr) (res any) { // = types.TypeAndValue | as
// - for an array or *array, use [n]int.
// The last two entail progressively stronger index checks.
var ct ast.Expr // type syntax for constraint
- switch t := t.(type) {
+ switch t := aliases.Unalias(t).(type) {
case *types.Map:
if types.IsInterface(t.Key()) {
ct = &ast.MapType{
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
index 7eaa6bff3f5..24ffb55fd1a 100644
--- a/internal/refactor/inline/inline.go
+++ b/internal/refactor/inline/inline.go
@@ -1154,7 +1154,7 @@ func arguments(caller *Caller, calleeDecl *ast.FuncDecl, assign1 func(*types.Var
// Make * or & explicit.
argIsPtr := isPointer(arg.typ)
- paramIsPtr := isPointer(seln.Obj().Type().(*types.Signature).Recv().Type())
+ paramIsPtr := isPointer(seln.Obj().Type().Underlying().(*types.Signature).Recv().Type())
if !argIsPtr && paramIsPtr {
// &recv
arg.expr = &ast.UnaryExpr{Op: token.AND, X: arg.expr}
diff --git a/internal/refactor/inline/util.go b/internal/refactor/inline/util.go
index 267ef745e32..7581ca29a50 100644
--- a/internal/refactor/inline/util.go
+++ b/internal/refactor/inline/util.go
@@ -132,7 +132,7 @@ func indirectSelection(seln *types.Selection) bool {
return true
}
- tParam := seln.Obj().Type().(*types.Signature).Recv().Type()
+ tParam := seln.Obj().Type().Underlying().(*types.Signature).Recv().Type()
return isPointer(tArg) && !isPointer(tParam) // implicit *
}
diff --git a/refactor/eg/eg_test.go b/refactor/eg/eg_test.go
index 4154e9a8f4e..36fd3add8a7 100644
--- a/refactor/eg/eg_test.go
+++ b/refactor/eg/eg_test.go
@@ -12,6 +12,7 @@ package eg_test
import (
"bytes"
"flag"
+ "go/build"
"go/constant"
"go/parser"
"go/token"
@@ -47,9 +48,12 @@ func Test(t *testing.T) {
t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
}
+ ctx := build.Default // copy
+ ctx.CgoEnabled = false // don't use cgo
conf := loader.Config{
Fset: token.NewFileSet(),
ParserMode: parser.ParseComments,
+ Build: &ctx,
}
// Each entry is a single-file package.